From 2f301e315a774658ed36f2face58cff089d21d4b Mon Sep 17 00:00:00 2001 From: Doug Toppin Date: Sun, 23 Feb 2014 21:08:13 -0500 Subject: [PATCH 01/23] Sending email was missing the from field --- lib/models/user.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/models/user.js b/lib/models/user.js index f35053b3..652051ea 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -303,6 +303,7 @@ User.prototype.verify = function (options, fn) { var template = loopback.template(options.template); Email.send({ to: options.to || user.email, + from: options.from, subject: options.subject || 'Thanks for Registering', text: options.text, html: template(options) From 42c9777de3c1a51b68e2d81b7b762e91a98b0455 Mon Sep 17 00:00:00 2001 From: Doug Toppin Date: Tue, 25 Feb 2014 22:14:32 -0500 Subject: [PATCH 02/23] using base64 caused an occasional token string to contain '+' which resulted in a space being embedded in the token. 'hex' should always produce a url safe string for the token. --- lib/models/user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/user.js b/lib/models/user.js index 652051ea..639d383d 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -281,7 +281,8 @@ User.prototype.verify = function (options, fn) { if(err) { fn(err); } else { - user.verificationToken = buf.toString('base64'); + // base64 may not produce a url safe string so we are using hex + user.verificationToken = buf.toString('hex'); user.save(function (err) { if(err) { fn(err); From d6a1270d87de14b07bc2b798118550ebd6087829 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 20 Mar 2014 16:35:42 -0700 Subject: [PATCH 03/23] Try to fix org.pegdown.ParsingTimeoutException --- docs/api-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-model.md b/docs/api-model.md index 0d54201b..5ec0beb2 100644 --- a/docs/api-model.md +++ b/docs/api-model.md @@ -49,7 +49,7 @@ var oracle = loopback.createDataSource({ User.attachTo(oracle); ``` -**Note:** until a model is attached to a data source it will **not** have any **attached methods**. +NOTE: until a model is attached to a data source it will not have any attached methods. ### Properties From c521b3c386cbde2fa52fb073d96ec2341c90ef4e Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Mar 2014 12:18:00 -0700 Subject: [PATCH 04/23] Allow app.model() to accept a DataSource instance --- lib/application.js | 8 +++- test/hidden-properties.test.js | 18 ++++++++ test/model.test.js | 77 ---------------------------------- test/remoting.integration.js | 3 +- 4 files changed, 25 insertions(+), 81 deletions(-) create mode 100644 test/hidden-properties.test.js diff --git a/lib/application.js b/lib/application.js index acb4f1f2..1705a4a5 100644 --- a/lib/application.js +++ b/lib/application.js @@ -90,7 +90,7 @@ app.disuse = function (route) { * * @param {String} modelName The name of the model to define * @options {Object} config The model's configuration - * @property {String} dataSource The `DataSource` to attach the model to + * @property {String|DataSource} dataSource The `DataSource` to attach the model to * @property {Object} [options] an object containing `Model` options * @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language) * @end @@ -545,7 +545,11 @@ function dataSourcesFromConfig(config) { function modelFromConfig(name, config, app) { var ModelCtor = require('./loopback').createModel(name, config.properties, config.options); - var dataSource = app.dataSources[config.dataSource]; + 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 +'"'); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js new file mode 100644 index 00000000..7664631c --- /dev/null +++ b/test/hidden-properties.test.js @@ -0,0 +1,18 @@ +var loopback = require('../'); + +describe('hidden properties', function () { + beforeEach(function (done) { + var app = this.app = loopback(); + var Product = this.Product = app.model('product', { + properties: { + secret: {} + }, + dataSource: loopback.memory() + }); + app.listen(done); + }); + + it('should hide a property remotely', function () { + + }); +}); diff --git a/test/model.test.js b/test/model.test.js index d291db2b..d1ed5097 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -624,81 +624,4 @@ describe('Model', function() { assert.equal(model, acl); }); }); - - // describe('Model.hasAndBelongsToMany()', function() { - // it("TODO: implement / document", function(done) { - // /* example - - // - // */ - // done(new Error('test not implemented')); - // }); - // }); - - // describe('Model.remoteMethods()', function() { - // it("Return a list of enabled remote methods", function() { - // app.model(User); - // User.remoteMethods(); // ['save', ...] - // }); - // }); - - // describe('Model.availableMethods()', function() { - // it("Returns the currently available api of a model as well as descriptions of any modified behavior or methods from attached data sources", function(done) { - // /* example - - // User.attachTo(oracle); - // console.log(User.availableMethods()); - // - // { - // 'User.all': { - // accepts: [{arg: 'filter', type: 'object', description: '...'}], - // returns: [{arg: 'users', type: ['User']}] - // }, - // 'User.find': { - // accepts: [{arg: 'id', type: 'any'}], - // returns: [{arg: 'items', type: 'User'}] - // }, - // ... - // } - // var oracle = loopback.createDataSource({ - // connector: 'oracle', - // host: '111.22.333.44', - // database: 'MYDB', - // username: 'username', - // password: 'password' - // }); - // - // */ - // done(new Error('test not implemented')); - // }); - // }); - -// describe('Model.before(name, fn)', function(){ -// it('Run a function before a method is called', function() { -// // User.before('save', function(user, next) { -// // console.log('about to save', user); -// // -// // next(); -// // }); -// // -// // User.before('delete', function(user, next) { -// // // prevent all delete calls -// // next(new Error('deleting is disabled')); -// // }); -// // User.beforeRemote('save', function(ctx, user, next) { -// // if(ctx.user.id === user.id) { -// // next(); -// // } else { -// // next(new Error('must be logged in to update')) -// // } -// // }); -// -// throw new Error('not implemented'); -// }); -// }); -// -// describe('Model.after(name, fn)', function(){ -// it('Run a function after a method is called', function() { -// -// throw new Error('not implemented'); -// }); -// }); }); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 02fcbde0..8fa987f2 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -66,5 +66,4 @@ describe('remoting - integration', function () { }); }); -}) -; +}); From 5b50a99eb366f2af36c6a5b6479adacc04fc1547 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Mar 2014 12:53:04 -0700 Subject: [PATCH 05/23] Add hidden property support to models --- lib/models/model.js | 16 ++++++++++++++++ lib/models/user.js | 1 + test/access-control.integration.js | 6 +++++- test/hidden-properties.test.js | 24 ++++++++++++++++++------ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/models/model.js b/lib/models/model.js index 78df36f0..7d52c3b4 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -200,6 +200,22 @@ Model._getAccessTypeForMethod = function(method) { } } +var superToJSON = Model.prototype.toJSON; +Model.prototype.toJSON = function toJSON() { + var Model = this.constructor; + var obj = superToJSON.apply(this, arguments); + var settings = Model.definition && Model.definition.settings; + var hiddenProperties = settings && settings.hidden; + + if(Array.isArray(hiddenProperties)) { + for(var i = 0; i < hiddenProperties.length; i++) { + delete obj[hiddenProperties[i]]; + } + } + + return obj; +} + // setup the initial model Model.setup(); diff --git a/lib/models/user.js b/lib/models/user.js index 049f1262..85aa6519 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -49,6 +49,7 @@ var properties = { }; var options = { + hidden: ['password'], acls: [ { principalType: ACL.ROLE, diff --git a/test/access-control.integration.js b/test/access-control.integration.js index e96de823..1abb320e 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -55,6 +55,7 @@ describe('access control - integration', function () { return '/api/accessTokens/' + this.randomToken.id; } }); + */ describe('/users', function () { @@ -94,6 +95,10 @@ describe('access control - integration', function () { }); lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() { lt.it.shouldBeAllowed(); + it('should not include a password', function() { + var user = this.res.body; + assert.equal(user.password, undefined); + }); }); lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() { lt.it.shouldBeAllowed(); @@ -136,7 +141,6 @@ describe('access control - integration', function () { return '/api/banks/' + this.bank.id; } }); - */ describe('/accounts', function () { lt.beforeEach.givenModel('account'); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js index 7664631c..65565341 100644 --- a/test/hidden-properties.test.js +++ b/test/hidden-properties.test.js @@ -4,15 +4,27 @@ describe('hidden properties', function () { beforeEach(function (done) { var app = this.app = loopback(); var Product = this.Product = app.model('product', { - properties: { - secret: {} - }, + options: {hidden: ['secret']}, dataSource: loopback.memory() }); - app.listen(done); + app.use(loopback.rest()); + + Product.create( + {name: 'pencil', secret: 'secret'}, + done + ); }); - it('should hide a property remotely', function () { - + it('should hide a property remotely', function (done) { + request(this.app) + .get('/products') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res){ + if(err) return done(err); + var product = res.body[0]; + assert.equal(product.secret, undefined); + done(); + }); }); }); From e52dbe2fb592a6481745bb6ed184c138934e9877 Mon Sep 17 00:00:00 2001 From: Doug Toppin Date: Sun, 23 Mar 2014 21:06:22 -0400 Subject: [PATCH 06/23] fix to enable ACL for confirm link sent by email --- lib/models/user.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/models/user.js b/lib/models/user.js index 639d383d..6e264b74 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -90,7 +90,13 @@ var options = { principalId: Role.OWNER, permission: ACL.ALLOW, property: "updateAttributes" - } + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.ALLOW, + property: "confirm" + } ], relations: { accessTokens: { From b1679803d9397c629d7c125a5c76857c86571497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 25 Mar 2014 16:47:07 +0100 Subject: [PATCH 07/23] test: add hasAndBelongsToMany integration test * it allows to find related object via URL scope GET /api/categories/{cat-id}/products * it allows to find related objects via where filter GET /api/products?filter[where][categoryId]={cat-id} (skipped for now) * it includes requested related models in `find` GET /api/categories/findOne ?filter[where][id]=CAT-ID&filter[include]=products * it includes requested related models in `findById` GET /api/categories/{cat-id}?include=products (skipped for now) --- test/relations.integration.js | 110 ++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/test/relations.integration.js b/test/relations.integration.js index 2726b306..fb1493e2 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -4,6 +4,7 @@ var path = require('path'); 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; describe('relations - integration', function () { @@ -95,4 +96,113 @@ describe('relations - integration', function () { }); }); + describe('hasAndBelongsToMany', function() { + beforeEach(function defineProductAndCategoryModels() { + var product = app.model( + 'product', + { properties: { id: 'string', name: 'string' }, dataSource: 'db' } + + ); + var category = app.model( + 'category', + { properties: { id: 'string', name: 'string' }, dataSource: 'db' } + ); + product.hasAndBelongsToMany(category); + category.hasAndBelongsToMany(product); + }); + + lt.beforeEach.givenModel('category'); + + beforeEach(function createProductsInCategory(done) { + var test = this; + this.category.products.create({ + name: 'a-product' + }, function(err, product) { + if (err) return done(err); + test.product = product; + done(); + }); + }); + + beforeEach(function createAnotherCategoryAndProduct(done) { + app.models.category.create({ name: 'another-category' }, + function(err, cat) { + if (err) return done(err); + cat.products.create({ name: 'another-product' }, done); + }); + }); + + afterEach(function(done) { + this.app.models.product.destroyAll(done); + }); + + it.skip('allows to find related objects via where filter', function(done) { + //TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94 + var expectedProduct = this.product; + // Note: the URL format is not final + this.get('/api/products?filter[where][categoryId]=' + this.category.id) + .expect(200, function(err, res) { + if (err) return done(err); + expect(res.body).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + + it('allows to find related object via URL scope', function(done) { + var expectedProduct = this.product; + this.get('/api/categories/' + this.category.id + '/products') + .expect(200, function(err, res) { + if (err) return done(err); + expect(res.body).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + + it('includes requested related models in `find`', function(done) { + var expectedProduct = this.product; + var url = '/api/categories/findOne?filter[where][id]=' + + this.category.id + '&filter[include]=products'; + + this.get(url) + .expect(200, function(err, res) { + expect(res.body).to.have.property('products'); + expect(res.body.products).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + + it.skip('includes requested related models in `findById`', function(done) { + //TODO https://github.com/strongloop/loopback-datasource-juggler/issues/93 + var expectedProduct = this.product; + // Note: the URL format is not final + var url = '/api/categories/' + this.category.id + '?include=products'; + + this.get(url) + .expect(200, function(err, res) { + expect(res.body).to.have.property('products'); + expect(res.body.products).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + }); }); From a08b047fab49ef60216af0769ef419289bf22689 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Mar 2014 13:01:53 -0700 Subject: [PATCH 08/23] Add hidden property documentation --- lib/application.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/application.js b/lib/application.js index 1705a4a5..f7090d81 100644 --- a/lib/application.js +++ b/lib/application.js @@ -92,6 +92,8 @@ app.disuse = function (route) { * @options {Object} config The model's configuration * @property {String|DataSource} dataSource The `DataSource` to attach the model to * @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) * @end * @returns {ModelConstructor} the model class From 8b71c3022d76084e43d3a1e89214e2227e6bf26a Mon Sep 17 00:00:00 2001 From: Doug Toppin Date: Sun, 30 Mar 2014 08:02:19 -0400 Subject: [PATCH 09/23] Update user.js Corrected spacing on confirm ACL, removed extraneous comment on using hex rather than base64 --- lib/models/user.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/models/user.js b/lib/models/user.js index 6e264b74..ba1dc104 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -91,12 +91,12 @@ var options = { permission: ACL.ALLOW, property: "updateAttributes" }, - { + { principalType: ACL.ROLE, principalId: Role.EVERYONE, permission: ACL.ALLOW, property: "confirm" - } + } ], relations: { accessTokens: { @@ -287,7 +287,6 @@ User.prototype.verify = function (options, fn) { if(err) { fn(err); } else { - // base64 may not produce a url safe string so we are using hex user.verificationToken = buf.toString('hex'); user.save(function (err) { if(err) { From f77378bdd1674209b20831898fe0567a36f5927a Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 31 Mar 2014 12:05:56 -0700 Subject: [PATCH 10/23] 1.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42d80bfe..945145b8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "Platform", "mBaaS" ], - "version": "1.7.2", + "version": "1.7.3", "scripts": { "test": "mocha -R spec" }, From 21547b9986b77e08180d32748cccdfa38dc1cd2c Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 2 Apr 2014 10:34:29 -0700 Subject: [PATCH 11/23] Add link to loopback.io --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 39b89125..46464037 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # LoopBack +For a quick introduction and overview, see http://loopback.io/. + ## Documentation [See the full documentation](http://docs.strongloop.com/display/DOC/LoopBack). From a265d6700737ef2bc6d7352bf7cc73cbad8d3886 Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 2 Apr 2014 15:15:21 -0700 Subject: [PATCH 12/23] Cleanup and update of jsdoc --- lib/application.js | 91 ++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/lib/application.js b/lib/application.js index f7090d81..4242caa3 100644 --- a/lib/application.js +++ b/lib/application.js @@ -48,8 +48,7 @@ var app = exports = module.exports = {}; /** * Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). * - * **NOTE:** Calling `app.remotes()` multiple times will only ever return a - * single set of remote objects. + * **NOTE:** Calling `app.remotes()` more than once returns only a single set of remote objects. * @returns {RemoteObjects} */ @@ -88,13 +87,13 @@ app.disuse = function (route) { * }); * ``` * - * @param {String} modelName The name of the model to define - * @options {Object} config The model's configuration - * @property {String|DataSource} dataSource The `DataSource` to attach the model to - * @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) + * @param {String} modelName The name of the model to define. + * @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). * @end * @returns {ModelConstructor} the model class */ @@ -129,14 +128,11 @@ app.model = function (Model, config) { } /** - * Get the models exported by the app. Only models defined using `app.model()` - * will show up in this list. + * Get the models exported by the app. Returns only models defined using `app.model()` * - * There are two ways how to access models. + * There are two ways to access models: * - * **1. A list of all models** - * - * Call `app.models()` to get a list of all models. + * 1. Call `app.models()` to get a list of all models. * * ```js * var models = app.models(); @@ -146,12 +142,11 @@ app.model = function (Model, config) { * }); * ``` * - * **2. By model name** - * + * **2. Use `app.model` to access a model by name. * `app.model` has properties for all defined models. * - * In the following example the `Product` and `CustomerReceipt` models are - * accessed using the `models` object. + * The following example illustrates accessing the `Product` and `CustomerReceipt` models + * using the `models` object. * * ```js * var loopback = require('loopback'); @@ -178,7 +173,7 @@ app.model = function (Model, config) { * var customerReceipt = app.models.customerReceipt; * ``` * - * @returns {Array} a list of model classes + * @returns {Array} Array of model classes. */ app.models = function () { @@ -200,6 +195,7 @@ app.dataSource = function (name, config) { /** * Get all remote objects. + * @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). */ app.remoteObjects = function () { @@ -220,8 +216,7 @@ app.remoteObjects = function () { /** * Enable swagger REST API documentation. * - * > Note: This method is deprecated, use the extension - * [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead. + * **Note**: This method is deprecated. Use [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead. * * **Options** * @@ -314,29 +309,30 @@ app.enableAuth = function() { /** * Initialize an application from an options object or a set of JSON and JavaScript files. * - * **What happens during an app _boot_?** + * This function takes an optional argument that is either a string or an object. * - * 1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory - * 2. **Models** are created from an `options.models` object or `models.json` in the current directory - * 3. Any JavaScript files in the `./models` directory are loaded with `require()`. - * 4. Any JavaScript files in the `./boot` directory are loaded with `require()`. + * If the argument is a string, then it sets the application root directory based on the string value. Then it: + * 1. Creates DataSources from the `datasources.json` file in the application root directory. + * 2. Creates Models from the `models.json` file in the application root directory. * - * **Options** - * - * - `appRootDir` - _optional_ - the directory to use when loading JSON and JavaScript files - * - `models` - _optional_ - an object containing `Model` definitions - * - `dataSources` - _optional_ - an object containing `DataSource` definitions + * If the argument is an object, then it looks for `model`, `dataSources`, and `appRootDir` properties of the object. + * If the object has no `appRootDir` property then it sets the current working directory as the application root directory. + * Then it: + * 1. Creates DataSources from the `options.dataSources` object. + * 2. Creates Models from the `options.models` object. * - * > **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple - * > files may result - * > in models being **undefined** due to race conditions. To avoid this when - * > using `app.boot()` - * > make sure all models are passed as part of the `models` definition. + * In both cases, the function loads JavaScript files in the `/models` and `/boot` subdirectories of the application root directory with `require()`. + * + * **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple + * files may result in models being **undefined** due to race conditions. + * To avoid this when using `app.boot()` make sure all models are passed as part of the `models` definition. + * + * Throws an error if the config object is not valid or if boot fails. * * * **Model Definitions** * - * The following is an example of an object containing two `Model` definitions: "location" and "inventory". + * The following is example JSON for two `Model` definitions: "dealership" and "location". * * ```js * { @@ -381,20 +377,13 @@ app.enableAuth = function() { * } * } * ``` - * - * **Model definition properties** - * - * - `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to - * - `options` - _optional_ - an object containing `Model` options - * - `properties` _optional_ - an object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language) - * - * **DataSource definition properties** - * - * - `connector` - **required** - the name of the [connector](#working-with-data-sources-and-connectors) + * @options {String|Object} options Boot options; If String, this is the application root directory; if object, has below properties. + * @property {String} appRootDir Directory to use when loading JSON and JavaScript files (optional). Defaults to the current directory (`process.cwd()`). + * @property {Object} models Object containing `Model` definitions (optional). + * @property {Object} dataSources Object containing `DataSource` definitions (optional). + * @end * * @header app.boot([options]) - * @throws {Error} If config is not valid - * @throws {Error} If boot fails */ app.boot = function(options) { @@ -799,7 +788,7 @@ app.installMiddleware = function() { * This way the port param contains always the real port number, even when * listen was called with port number 0. * - * @param {Function=} cb If specified, the callback will be added as a listener + * @param {Function} cb If specified, the callback is added as a listener * for the server's "listening" event. * @returns {http.Server} A node `http.Server` with this application configured * as the request handler. From 006aca6b9c1e4917fc1dc8b8d8d555dc8d735f53 Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 2 Apr 2014 15:46:39 -0700 Subject: [PATCH 13/23] Update and cleanup JSDoc --- lib/loopback.js | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/loopback.js b/lib/loopback.js index 6a057a44..be705340 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -15,27 +15,29 @@ var express = require('express') , assert = require('assert'); /** - * `loopback` is the main entry for LoopBack core module. It provides static + * Main entry for LoopBack core module. It provides static properties and * methods to create models and data sources. The module itself is a function * that creates loopback `app`. For example, * - * * ```js * var loopback = require('loopback'); * var app = loopback(); * ``` + * + * @class loopback + * @header loopback */ var loopback = exports = module.exports = createApplication; /** - * Is this a browser environment? + * True if running in a browser environment; false otherwise. */ loopback.isBrowser = typeof window !== 'undefined'; /** - * Is this a server environment? + * True if running in a server environment; false otherwise. */ loopback.isServer = !loopback.isBrowser; @@ -116,11 +118,10 @@ loopback.errorHandler.title = 'Loopback'; /** * Create a data source with passing the provided options to the connector. * - * @param {String} name (optional) - * @param {Object} options - * - * - connector - an loopback connector - * - other values - see the specified `connector` docs + * @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) { @@ -141,7 +142,7 @@ loopback.createDataSource = function (name, options) { /** * Create a named vanilla JavaScript class constructor with an attached set of properties and options. * - * @param {String} name - must be unique + * @param {String} name Unique name. * @param {Object} properties * @param {Object} options (optional) */ @@ -223,7 +224,7 @@ loopback.memory = function (name) { /** * Look up a model class by name from all models created by loopback.createModel() * @param {String} modelName The model name - * @return {Model} The model class + * @returns {Model} The model class */ loopback.getModel = function(modelName) { return loopback.Model.modelBuilder.models[modelName]; @@ -233,7 +234,7 @@ loopback.getModel = function(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 - * @return {Model} The subclass if found or the base 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'); @@ -250,7 +251,7 @@ loopback.getModelByType = function(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 - * @return {DataSource} The data source instance + * @returns {DataSource} The data source instance */ loopback.setDefaultDataSourceForType = function(type, dataSource) { @@ -267,7 +268,7 @@ loopback.setDefaultDataSourceForType = function(type, dataSource) { /** * Get the default `dataSource` for a given `type`. * @param {String} type The datasource type - * @return {DataSource} The data source instance + * @returns {DataSource} The data source instance */ loopback.getDefaultDataSourceForType = function(type) { From 4c246c7f2c84f835fe8f0a631ffde468c4da430f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 3 Apr 2014 10:39:42 +0200 Subject: [PATCH 14/23] Describe the "id" parameter of model's sharedCtor Extend remoting metadata to describe the "id" parameter accepted by the sharedCtor of all models. --- lib/models/model.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/model.js b/lib/models/model.js index 7d52c3b4..bb59b129 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -100,8 +100,9 @@ Model.setup = function () { }; // Map the prototype method to /:id with data in the body + var idDesc = ModelCtor.modelName + ' id'; ModelCtor.sharedCtor.accepts = [ - {arg: 'id', type: 'any', http: {source: 'path'}} + {arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc} // {arg: 'instance', type: 'object', http: {source: 'body'}} ]; From dff182ed2710a4393fec98d1cb50b15720e84aa5 Mon Sep 17 00:00:00 2001 From: crandmck Date: Thu, 3 Apr 2014 11:29:00 -0700 Subject: [PATCH 15/23] Clean up JSDoc comments. Remove doc for deprecated installMiddleware function --- lib/application.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/application.js b/lib/application.js index 4242caa3..54d509ef 100644 --- a/lib/application.js +++ b/lib/application.js @@ -639,7 +639,8 @@ function clearHandlerCache(app) { app._handlers = undefined; } -/** +/*! + * This function is now deprecated. * Install all express middleware required by LoopBack. * * It is possible to inject your own middleware by listening on one of the From cdbf45c53a856eb366a399850d865696c0b5c4ba Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 3 Apr 2014 20:43:49 -0700 Subject: [PATCH 16/23] Add an integration test for belongsTo remoting --- .../simple-integration-app/models.json | 10 +++++++- test/relations.integration.js | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/test/fixtures/simple-integration-app/models.json b/test/fixtures/simple-integration-app/models.json index 10a28a01..494367d7 100644 --- a/test/fixtures/simple-integration-app/models.json +++ b/test/fixtures/simple-integration-app/models.json @@ -30,7 +30,15 @@ "widget": { "properties": {}, "public": true, - "dataSource": "db" + "dataSource": "db", + "options": { + "relations": { + "store": { + "model": "store", + "type": "belongsTo" + } + } + } }, "store": { "properties": {}, diff --git a/test/relations.integration.js b/test/relations.integration.js index fb1493e2..c6decb26 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -96,6 +96,29 @@ describe('relations - integration', function () { }); }); + describe('/widgets/:id/store', function () { + beforeEach(function (done) { + var self = this; + this.store.widgets.create({ + name: this.widgetName + }, function(err, widget) { + self.widget = widget; + self.url = '/api/widgets/' + self.widget.id + '/store'; + done(); + }); + }); + lt.describe.whenCalledRemotely('GET', '/api/widgets/:id', function () { + it('should succeed with statusCode 200', function () { + assert.equal(this.res.statusCode, 200); + }); + }); + lt.describe.whenCalledRemotely('GET', '/api/widgets/:id/store', function () { + it('should succeed with statusCode 200', function () { + assert.equal(this.res.statusCode, 200); + }); + }); + }); + describe('hasAndBelongsToMany', function() { beforeEach(function defineProductAndCategoryModels() { var product = app.model( From 3640b2e113ed71a64e286def067c753ae7140d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 4 Apr 2014 16:41:25 +0200 Subject: [PATCH 17/23] v1.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 945145b8..b9bf2ad9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "Platform", "mBaaS" ], - "version": "1.7.3", + "version": "1.7.4", "scripts": { "test": "mocha -R spec" }, From 8f50c9dede1f89570bac166c8568cb8031c3bbc8 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 4 Apr 2014 08:26:57 -0700 Subject: [PATCH 18/23] Add an assertion to the returned store object --- test/relations.integration.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/relations.integration.js b/test/relations.integration.js index c6decb26..0c3df8df 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -107,14 +107,10 @@ describe('relations - integration', function () { done(); }); }); - lt.describe.whenCalledRemotely('GET', '/api/widgets/:id', function () { - it('should succeed with statusCode 200', function () { - assert.equal(this.res.statusCode, 200); - }); - }); lt.describe.whenCalledRemotely('GET', '/api/widgets/:id/store', function () { it('should succeed with statusCode 200', function () { assert.equal(this.res.statusCode, 200); + assert.equal(this.res.body.id, this.store.id); }); }); }); From 1c1364636d6042ee82dcbc7af828c6090851d721 Mon Sep 17 00:00:00 2001 From: Alex Pica Date: Thu, 10 Apr 2014 06:01:58 +0300 Subject: [PATCH 19/23] Fix #229 (Whitespaces removed --- lib/models/user.js | 49 ++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/models/user.js b/lib/models/user.js index c077ad35..caba6f70 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -112,7 +112,7 @@ var options = { * Extends from the built in `loopback.Model` type. * * Default `User` ACLs. - * + * * - DENY EVERYONE `*` * - ALLOW EVERYONE `create` * - ALLOW OWNER `removeById` @@ -160,11 +160,11 @@ User.login = function (credentials, include, fn) { err.statusCode = 400; return fn(err); } - + this.findOne({where: query}, function(err, user) { var defaultError = new Error('login failed'); defaultError.statusCode = 401; - + if(err) { debug('An error is reported from User.findOne: %j', err); fn(defaultError); @@ -269,7 +269,7 @@ User.prototype.verify = function (options, fn) { assert(options.type === 'email', 'Unsupported verification type'); assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); - + options.redirect = options.redirect || '/'; options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); options.user = this; @@ -280,13 +280,16 @@ User.prototype.verify = function (options, fn) { + '://' + options.host + User.http.path - + User.confirm.http.path; - + + User.confirm.http.path + + '?uid=' + + options.user.id + + '&redirect=' + + options.redirect; + - // Email model var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); - + crypto.randomBytes(64, function(err, buf) { if(err) { fn(err); @@ -296,20 +299,20 @@ User.prototype.verify = function (options, fn) { if(err) { fn(err); } else { - sendEmail(user); + sendEmail(user); } }); } }); - + // TODO - support more verification types function sendEmail(user) { - options.verifyHref += '?token=' + user.verificationToken; - + options.verifyHref += '&token=' + user.verificationToken; + options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; - + options.text = options.text.replace('{href}', options.verifyHref); - + var template = loopback.template(options.template); Email.send({ to: options.to || user.email, @@ -412,7 +415,7 @@ User.setup = function () { // We need to call the base class's setup method Model.setup.call(this); var UserModel = this; - + // max ttl this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; this.settings.ttl = DEFAULT_TTL; @@ -421,7 +424,7 @@ User.setup = function () { var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); this.$password = bcrypt.hashSync(plain, salt); } - + loopback.remoteMethod( UserModel.login, { @@ -441,7 +444,7 @@ User.setup = function () { http: {verb: 'post'} } ); - + loopback.remoteMethod( UserModel.logout, { @@ -460,7 +463,7 @@ User.setup = function () { http: {verb: 'all'} } ); - + loopback.remoteMethod( UserModel.confirm, { @@ -472,7 +475,7 @@ User.setup = function () { http: {verb: 'get', path: '/confirm'} } ); - + loopback.remoteMethod( UserModel.resetPassword, { @@ -492,16 +495,16 @@ User.setup = function () { } }); }); - + // default models UserModel.email = require('./email'); UserModel.accessToken = require('./access-token'); - + UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - + UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); - + return UserModel; } From e9323ce6645078b392813d2577e0255b11c63ea1 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 11 Apr 2014 09:31:59 -0700 Subject: [PATCH 20/23] 1.7.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9bf2ad9..de2512a2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "Platform", "mBaaS" ], - "version": "1.7.4", + "version": "1.7.5", "scripts": { "test": "mocha -R spec" }, From 63ad8f69ae75377a9020a6fa0f5a6398cd6342b7 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 11 Apr 2014 11:40:53 -0700 Subject: [PATCH 21/23] Add test for remoting nested hidden properties --- lib/models/model.js | 16 -------------- test/hidden-properties.test.js | 39 +++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/lib/models/model.js b/lib/models/model.js index bb59b129..fdc48f55 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -201,22 +201,6 @@ Model._getAccessTypeForMethod = function(method) { } } -var superToJSON = Model.prototype.toJSON; -Model.prototype.toJSON = function toJSON() { - var Model = this.constructor; - var obj = superToJSON.apply(this, arguments); - var settings = Model.definition && Model.definition.settings; - var hiddenProperties = settings && settings.hidden; - - if(Array.isArray(hiddenProperties)) { - for(var i = 0; i < hiddenProperties.length; i++) { - delete obj[hiddenProperties[i]]; - } - } - - return obj; -} - // setup the initial model Model.setup(); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js index 65565341..356c20f7 100644 --- a/test/hidden-properties.test.js +++ b/test/hidden-properties.test.js @@ -7,14 +7,28 @@ describe('hidden properties', function () { options: {hidden: ['secret']}, dataSource: loopback.memory() }); + var Category = this.Category = this.app.model('category', { + dataSource: loopback.memory() + }); + Category.hasMany(Product); app.use(loopback.rest()); - - Product.create( - {name: 'pencil', secret: 'secret'}, - done - ); + Category.create({ + name: 'my category' + }, function(err, category) { + category.products.create({ + name: 'pencil', + secret: 'a secret' + }, done); + }); }); + afterEach(function(done) { + var Product = this.Product; + this.Category.destroyAll(function() { + Product.destroyAll(done); + }); + }) + it('should hide a property remotely', function (done) { request(this.app) .get('/products') @@ -27,4 +41,19 @@ describe('hidden properties', function () { done(); }); }); + + it('should hide a property of nested models', function (done) { + var app = this.app; + request(app) + .get('/categories?filter[include]=products') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res){ + if(err) return done(err); + var category = res.body[0]; + var product = category.products[0]; + assert.equal(product.secret, undefined); + done(); + }); + }); }); From 37a28db01780a34bc8f7200337846250841e9d8a Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 11 Apr 2014 12:42:59 -0700 Subject: [PATCH 22/23] Bump juggler version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index de2512a2..3822fbd6 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,10 @@ "async": "~0.2.10" }, "peerDependencies": { - "loopback-datasource-juggler": "~1.3.3" + "loopback-datasource-juggler": "~1.3.11" }, "devDependencies": { - "loopback-datasource-juggler": "~1.3.3", + "loopback-datasource-juggler": "~1.3.11", "mocha": "~1.17.1", "strong-task-emitter": "0.0.x", "supertest": "~0.9.0", From 5ffc0e8979c551621295f0f8051c95abf7a5b401 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 14 Apr 2014 10:29:40 -0700 Subject: [PATCH 23/23] 1.7.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3822fbd6..4f396937 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "Platform", "mBaaS" ], - "version": "1.7.5", + "version": "1.7.6", "scripts": { "test": "mocha -R spec" },