From 2f301e315a774658ed36f2face58cff089d21d4b Mon Sep 17 00:00:00 2001 From: Doug Toppin Date: Sun, 23 Feb 2014 21:08:13 -0500 Subject: [PATCH 01/10] 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/10] 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 de5d0b89494d96d6a6cdfeffa7f436a15b2c3e2b Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Mar 2014 12:02:11 -0700 Subject: [PATCH 03/10] Make verifications url safe --- lib/models/user.js | 2 +- test/fixtures/simple-integration-app/app.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/models/user.js b/lib/models/user.js index 049f1262..80147cf9 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -284,7 +284,7 @@ User.prototype.verify = function (options, fn) { if(err) { fn(err); } else { - user.verificationToken = buf.toString('base64'); + user.verificationToken = buf.toString('hex'); user.save(function (err) { if(err) { fn(err); diff --git a/test/fixtures/simple-integration-app/app.js b/test/fixtures/simple-integration-app/app.js index e460862a..5f71b1d3 100644 --- a/test/fixtures/simple-integration-app/app.js +++ b/test/fixtures/simple-integration-app/app.js @@ -7,7 +7,6 @@ app.use(loopback.favicon()); app.use(loopback.cookieParser({secret: app.get('cookieSecret')})); var apiPath = '/api'; app.use(apiPath, loopback.rest()); -app.use(app.router); app.use(loopback.static(path.join(__dirname, 'public'))); app.use(loopback.urlNotFound()); app.use(loopback.errorHandler()); From c521b3c386cbde2fa52fb073d96ec2341c90ef4e Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Mar 2014 12:18:00 -0700 Subject: [PATCH 04/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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" },