diff --git a/lib/application.js b/lib/application.js index acb4f1f2..f7090d81 100644 --- a/lib/application.js +++ b/lib/application.js @@ -90,8 +90,10 @@ 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 {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 @@ -545,7 +547,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/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..c077ad35 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, @@ -90,6 +91,12 @@ var options = { principalId: Role.OWNER, permission: ACL.ALLOW, property: "updateAttributes" + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.ALLOW, + property: "confirm" } ], relations: { @@ -284,7 +291,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); @@ -306,6 +313,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) 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" }, 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/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()); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js new file mode 100644 index 00000000..65565341 --- /dev/null +++ b/test/hidden-properties.test.js @@ -0,0 +1,30 @@ +var loopback = require('../'); + +describe('hidden properties', function () { + beforeEach(function (done) { + var app = this.app = loopback(); + var Product = this.Product = app.model('product', { + options: {hidden: ['secret']}, + dataSource: loopback.memory() + }); + app.use(loopback.rest()); + + Product.create( + {name: 'pencil', secret: 'secret'}, + done + ); + }); + + 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(); + }); + }); +}); 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/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(); + }); + }); + }); }); 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 () { }); }); -}) -; +});