Merge branch 'release/2.0.0' into production

This commit is contained in:
Miroslav Bajtoš 2014-07-22 20:38:23 +02:00
commit a3720bc05f
15 changed files with 240 additions and 191 deletions

View File

@ -4,13 +4,11 @@
var DataSource = require('loopback-datasource-juggler').DataSource
, registry = require('./registry')
, compat = require('./compat')
, assert = require('assert')
, fs = require('fs')
, extend = require('util')._extend
, _ = require('underscore')
, RemoteObjects = require('strong-remoting')
, swagger = require('strong-remoting/ext/swagger')
, stringUtils = require('underscore.string')
, path = require('path');
@ -267,34 +265,6 @@ app.remoteObjects = function () {
return result;
}
/**
* Enable swagger REST API documentation.
*
* **Note**: This method is deprecated. Use [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
*
* **Options**
*
* - `basePath` The basepath for your API - eg. 'http://localhost:3000'.
*
* **Example**
*
* ```js
* // enable docs
* app.docs({basePath: 'http://localhost:3000'});
* ```
*
* Run your app then navigate to
* [the API explorer](http://petstore.swagger.wordnik.com/).
* Enter your API basepath to view your generated docs.
*
* @deprecated
*/
app.docs = function (options) {
var remotes = this.remotes();
swagger(remotes, options);
}
/*!
* Get a handler of the specified type from the handler cache.
*/

View File

@ -1,56 +0,0 @@
var assert = require('assert');
/**
* Compatibility layer allowing applications based on an older LoopBack version
* to work with newer versions with minimum changes involved.
*
* You should not use it unless migrating from an older version of LoopBack.
*/
var compat = exports;
/**
* LoopBack versions pre-1.6 use plural model names when registering shared
* classes with strong-remoting. As the result, strong-remoting use method names
* like `Users.create` for the javascript methods like `User.create`.
* This has been fixed in v1.6, LoopBack consistently uses the singular
* form now.
*
* Turn this option on to enable the old behaviour.
*
* - `app.remotes()` and `app.remoteObjects()` will be indexed using
* plural names (Users instead of User).
*
* - Remote hooks must use plural names for the class name, i.e
* `Users.create` instead of `User.create`. This is transparently
* handled by `Model.beforeRemote()` and `Model.afterRemote()`.
*
* @type {boolean}
* @deprecated Your application should not depend on the way how loopback models
* and strong-remoting are wired together. It if does, you should update
* it to use singular model names.
*/
compat.usePluralNamesForRemoting = false;
/**
* Get the class name to use with strong-remoting.
* @param {function} Ctor Model class (constructor), e.g. `User`
* @return {string} Singular or plural name, depending on the value
* of `compat.usePluralNamesForRemoting`
* @internal
*/
compat.getClassNameForRemoting = function(Ctor) {
assert(
typeof(Ctor) === 'function',
'compat.getClassNameForRemoting expects a constructor as the argument');
if (compat.usePluralNamesForRemoting) {
assert(Ctor.pluralModelName,
'Model must have a "pluralModelName" property in compat mode');
return Ctor.pluralModelName;
}
return Ctor.modelName;
};

View File

@ -58,7 +58,12 @@ MailConnector.prototype.setupTransport = function(setting) {
var connector = this;
connector.transports = connector.transports || [];
connector.transportsIndex = connector.transportsIndex || {};
var transport = mailer.createTransport(setting.type, setting);
var transportModuleName = 'nodemailer-' + (setting.type || 'STUB').toLowerCase() + '-transport';
var transportModule = require(transportModuleName);
var transport = mailer.createTransport(transportModule(setting));
connector.transportsIndex[setting.type] = transport;
connector.transports.push(transport);
}

View File

@ -4,7 +4,6 @@
var assert = require('assert');
var remoting = require('strong-remoting');
var compat = require('../compat');
var DataAccessObject = require('loopback-datasource-juggler/lib/dao');
/**

View File

@ -13,8 +13,9 @@ function safeRequire(m) {
function createMiddlewareNotInstalled(memberName, moduleName) {
return function () {
throw new Error('The middleware loopback.' + memberName + ' is not installed.\n' +
'Please run `npm install ' + moduleName + '` to fix the problem.');
var msg = 'The middleware loopback.' + memberName + ' is not installed.\n' +
'Run `npm install --save ' + moduleName + '` to fix the problem.';
throw new Error(msg);
};
}

View File

@ -38,11 +38,6 @@ loopback.version = require('../package.json').version;
loopback.mime = express.mime;
/*!
* Compatibility layer, intentionally left undocumented.
*/
loopback.compat = require('./compat');
/*!
* Create an loopback application.
*

View File

@ -2,7 +2,6 @@
* Module Dependencies.
*/
var registry = require('../registry');
var compat = require('../compat');
var assert = require('assert');
var SharedClass = require('strong-remoting').SharedClass;
@ -87,7 +86,7 @@ Model.setup = function () {
// create a sharedClass
var sharedClass = ModelCtor.sharedClass = new SharedClass(
compat.getClassNameForRemoting(ModelCtor),
ModelCtor.modelName,
ModelCtor,
options.remoting
);
@ -152,7 +151,7 @@ Model.setup = function () {
var self = this;
if(this.app) {
var remotes = this.app.remotes();
var className = compat.getClassNameForRemoting(self);
var className = self.modelName;
remotes.before(className + '.' + name, function (ctx, next) {
fn(ctx, ctx.result, next);
});
@ -169,7 +168,7 @@ Model.setup = function () {
var self = this;
if(this.app) {
var remotes = this.app.remotes();
var className = compat.getClassNameForRemoting(self);
var className = self.modelName;
remotes.after(className + '.' + name, function (ctx, next) {
fn(ctx, ctx.result, next);
});
@ -184,12 +183,17 @@ Model.setup = function () {
// resolve relation functions
sharedClass.resolve(function resolver(define) {
var relations = ModelCtor.relations;
if(!relations) return;
if (!relations) {
return;
}
// get the relations
for(var relationName in relations) {
for (var relationName in relations) {
var relation = relations[relationName];
if(relation.type === 'belongsTo') {
if (relation.type === 'belongsTo') {
ModelCtor.belongsToRemoting(relationName, relation, define)
} else if (relation.type === 'hasMany') {
ModelCtor.hasManyRemoting(relationName, relation, define);
ModelCtor.scopeRemoting(relationName, relation, define);
} else {
ModelCtor.scopeRemoting(relationName, relation, define);
}
@ -342,6 +346,78 @@ Model.belongsToRemoting = function(relationName, relation, define) {
}, fn);
}
Model.hasManyRemoting = function (relationName, relation, define) {
var findByIdFunc = this.prototype['__findById__' + relationName];
define('__findById__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + relationName + '/:fk'},
accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}},
description: 'Find a related item by id for ' + relationName,
returns: {arg: 'result', type: relation.modelTo.modelName, root: true}
}, findByIdFunc);
var destroyByIdFunc = this.prototype['__destroyById__' + relationName];
define('__destroyById__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + relationName + '/:fk'},
accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}},
description: 'Delete a related item by id for ' + relationName,
returns: {}
}, destroyByIdFunc);
var updateByIdFunc = this.prototype['__updateById__' + relationName];
define('__updateById__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + relationName + '/:fk'},
accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}},
description: 'Update a related item by id for ' + relationName,
returns: {arg: 'result', type: relation.modelTo.modelName, root: true}
}, updateByIdFunc);
if (relation.modelThrough) {
var addFunc = this.prototype['__link__' + relationName];
define('__link__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + relationName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}},
description: 'Add a related item by id for ' + relationName,
returns: {arg: relationName, type: relation.modelThrough.modelName, root: true}
}, addFunc);
var removeFunc = this.prototype['__unlink__' + relationName];
define('__unlink__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + relationName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}},
description: 'Remove the ' + relationName + ' relation to an item by id',
returns: {}
}, removeFunc);
// FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD?
// true --> 200 and false --> 404?
var existsFunc = this.prototype['__exists__' + relationName];
define('__exists__' + relationName, {
isStatic: false,
http: {verb: 'head', path: '/' + relationName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}},
description: 'Check the existence of ' + relationName + ' relation to an item by id',
returns: {}
}, existsFunc);
}
};
Model.scopeRemoting = function(relationName, relation, define) {
var toModelName = relation.modelTo.modelName;

View File

@ -358,18 +358,25 @@ User.confirm = function (uid, token, redirect, fn) {
if(err) {
fn(err);
} else {
if(user.verificationToken === token) {
if(user && user.verificationToken === token) {
user.verificationToken = undefined;
user.emailVerified = true;
user.save(function (err) {
if(err) {
fn(err)
fn(err);
} else {
fn();
}
});
} else {
fn(new Error('invalid token'));
if (user) {
err = new Error('Invalid token: ' + token);
err.statusCode = 400;
} else {
err = new Error('User not found: ' + uid);
err.statusCode = 404;
}
fn(err);
}
}
});
@ -451,6 +458,7 @@ User.setup = function () {
loopback.remoteMethod(
UserModel.login,
{
description: 'Login a user with username/email and password',
accepts: [
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
{arg: 'include', type: 'string', http: {source: 'query' }, description:
@ -471,6 +479,7 @@ User.setup = function () {
loopback.remoteMethod(
UserModel.logout,
{
description: 'Logout a user with access token',
accepts: [
{arg: 'access_token', type: 'string', required: true, http: function(ctx) {
var req = ctx && ctx.req;
@ -490,6 +499,7 @@ User.setup = function () {
loopback.remoteMethod(
UserModel.confirm,
{
description: 'Confirm a user registration with email verification token',
accepts: [
{arg: 'uid', type: 'string', required: true},
{arg: 'token', type: 'string', required: true},
@ -502,6 +512,7 @@ User.setup = function () {
loopback.remoteMethod(
UserModel.resetPassword,
{
description: 'Reset password for a user with email',
accepts: [
{arg: 'options', type: 'object', required: true, http: {source: 'body'}}
],
@ -523,10 +534,12 @@ User.setup = function () {
UserModel.email = require('./email');
UserModel.accessToken = require('./access-token');
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
// email validation regex
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.validatesUniquenessOf('email', {message: 'Email already exists'});
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
return UserModel;
}

View File

@ -239,6 +239,19 @@ registry.createDataSource = function (name, options) {
var self = this;
var ds = new DataSource(name, options, self.modelBuilder);
ds.createModel = function (name, properties, settings) {
settings = settings || {};
var BaseModel = settings.base || settings.super;
if (!BaseModel) {
// Check the connector types
var connectorTypes = ds.connector && ds.connector.getTypes();
if (Array.isArray(connectorTypes) && connectorTypes.indexOf('db') !== -1) {
// Only set up the base model to PersistedModel if the connector is DB
BaseModel = self.PersistedModel;
} else {
BaseModel = self.Model;
}
settings.base = BaseModel;
}
var ModelCtor = self.createModel(name, properties, settings);
ModelCtor.attachTo(ds);
return ModelCtor;

View File

@ -26,7 +26,7 @@
"mobile",
"mBaaS"
],
"version": "2.0.0-beta7",
"version": "2.0.0",
"scripts": {
"test": "grunt mocha-and-karma"
},
@ -36,17 +36,18 @@
"canonical-json": "0.0.4",
"ejs": "~1.0.0",
"express": "4.x",
"strong-remoting": "~2.0.0-beta5",
"strong-remoting": "^2.0.0",
"bcryptjs": "~2.0.1",
"debug": "~1.0.4",
"inflection": "~1.3.8",
"nodemailer": "~0.7.1",
"nodemailer": "~1.0.1",
"nodemailer-stub-transport": "~0.1.4",
"uid2": "0.0.3",
"underscore": "~1.6.0",
"underscore.string": "~2.3.3"
},
"peerDependencies": {
"loopback-datasource-juggler": "~2.0.0-beta3"
"loopback-datasource-juggler": "^2.0.0"
},
"devDependencies": {
"browserify": "~4.2.1",
@ -71,7 +72,7 @@
"karma-phantomjs-launcher": "~0.1.4",
"karma-script-launcher": "~0.1.0",
"loopback-boot": "^1.1.0",
"loopback-datasource-juggler": "~2.0.0-beta3",
"loopback-datasource-juggler": "^2.0.0",
"loopback-testing": "~0.2.0",
"mocha": "~1.20.1",
"serve-favicon": "~2.0.1",

View File

@ -57,28 +57,6 @@ describe('app', function() {
request(app).get('/colors').expect(200, done);
});
});
describe('in compat mode', function() {
before(function() {
loopback.compat.usePluralNamesForRemoting = true;
});
after(function() {
loopback.compat.usePluralNamesForRemoting = false;
});
it('uses plural name as shared class name', function() {
var Color = db.createModel('color', {name: String});
app.model(Color);
var classes = app.remotes().classes().map(function(c) {return c.name});
expect(classes).to.contain('colors');
});
it('uses plural name as app.remoteObjects() key', function() {
var Color = db.createModel('color', {name: String});
app.model(Color);
expect(app.remoteObjects()).to.eql({ colors: Color });
});
});
});
describe('app.model(name, config)', function () {

View File

@ -33,6 +33,37 @@ describe('DataSource', function() {
assert.isFunc(Color.prototype, 'updateAttributes');
assert.isFunc(Color.prototype, 'reload');
});
it("should honor settings.base", function() {
var Base = memory.createModel('base');
var Color = memory.createModel('color', {name: String}, {base: Base});
assert(Color.prototype instanceof Base);
assert.equal(Color.base, Base);
});
it("should use loopback.PersistedModel as the base for DBs", function() {
var Color = memory.createModel('color', {name: String});
assert(Color.prototype instanceof loopback.PersistedModel);
assert.equal(Color.base, loopback.PersistedModel);
});
it("should use loopback.Model as the base for non DBs", function() {
// Mock up a non-DB connector
var Connector = function() {
};
Connector.prototype.getTypes = function() {
return ['rest'];
};
var ds = loopback.createDataSource({
connector: new Connector()
});
var Color = ds.createModel('color', {name: String});
assert(Color.prototype instanceof loopback.Model);
assert.equal(Color.base, loopback.Model);
});
});
describe.skip('PersistedModel Methods', function() {

View File

@ -24,7 +24,8 @@ describe('Email and SMTP', function () {
};
MyEmail.send(options, function(err, mail) {
assert(mail.message);
assert(!err);
assert(mail.response);
assert(mail.envelope);
assert(mail.messageId);
done(err);
@ -41,7 +42,7 @@ describe('Email and SMTP', function () {
});
message.send(function (err, mail) {
assert(mail.message);
assert(mail.response);
assert(mail.envelope);
assert(mail.messageId);
done(err);

View File

@ -270,37 +270,6 @@ describe.onServer('Remote Methods', function(){
});
})
describe('in compat mode', function() {
before(function() {
loopback.compat.usePluralNamesForRemoting = true;
});
after(function() {
loopback.compat.usePluralNamesForRemoting = false;
});
it('correctly install before/after hooks', function(done) {
var hooksCalled = [];
User.beforeRemote('**', function(ctx, user, next) {
hooksCalled.push('beforeRemote');
next();
});
User.afterRemote('**', function(ctx, user, next) {
hooksCalled.push('afterRemote');
next();
});
request(app).get('/users')
.expect(200, function(err, res) {
if (err) return done(err);
expect(hooksCalled, 'hooks called')
.to.eql(['beforeRemote', 'afterRemote']);
done();
});
});
});
describe('Model.hasMany(Model)', function() {
it("Define a one to many relationship", function(done) {
var Book = dataSource.createModel('book', {title: String, author: String});

View File

@ -109,6 +109,15 @@ describe('User', function(){
});
});
});
it('Requires a unique username', function(done) {
User.create({email: 'a@b.com', username: 'abc', password: 'foobar'}, function () {
User.create({email: 'b@b.com', username: 'abc', password: 'batbaz'}, function (err) {
assert(err, 'should error because the username is not unique!');
done();
});
});
});
it('Requires a password to login with basic auth', function(done) {
User.create({email: 'b@c.com'}, function (err) {
@ -426,7 +435,7 @@ describe('User', function(){
});
describe('Verification', function(){
describe('user.verify(options, fn)', function(){
it('Verify a user\'s email address', function(done) {
User.afterRemote('create', function(ctx, user, next) {
@ -443,11 +452,9 @@ describe('User', function(){
user.verify(options, function (err, result) {
assert(result.email);
assert(result.email.message);
assert(result.email.response);
assert(result.token);
assert(~result.email.message.indexOf('To: bar@bat.com'));
assert(~result.email.response.toString('utf-8').indexOf('To: bar@bat.com'));
done();
});
});
@ -462,13 +469,15 @@ describe('User', function(){
});
});
});
describe('User.confirm(options, fn)', function(){
it('Confirm a user verification', function(done) {
User.afterRemote('create', function(ctx, user, next) {
describe('User.confirm(options, fn)', function () {
var options;
function testConfirm(testFunc, done) {
User.afterRemote('create', function (ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
@ -476,29 +485,73 @@ describe('User', function(){
protocol: ctx.req.protocol,
host: ctx.req.get('host')
};
user.verify(options, function (err, result) {
if(err) return done(err);
request(app)
.get('/users/confirm?uid=' + result.uid + '&token=' + encodeURIComponent(result.token) + '&redirect=' + encodeURIComponent(options.redirect))
.expect(302)
.expect('location', options.redirect)
.end(function(err, res){
if(err) return done(err);
done();
});
if (err) {
return done(err);
}
testFunc(result, done);
});
});
request(app)
.post('/users')
.expect('Content-Type', /json/)
.expect(302)
.send({email: 'bar@bat.com', password: 'bar'})
.end(function(err, res){
if(err) return done(err);
.end(function (err, res) {
if (err) {
return done(err);
}
});
}
it('Confirm a user verification', function (done) {
testConfirm(function (result, done) {
request(app)
.get('/users/confirm?uid=' + (result.uid )
+ '&token=' + encodeURIComponent(result.token)
+ '&redirect=' + encodeURIComponent(options.redirect))
.expect(302)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
}, done);
});
it('Report error for invalid user id during verification', function (done) {
testConfirm(function (result, done) {
request(app)
.get('/users/confirm?uid=' + (result.uid + '_invalid')
+ '&token=' + encodeURIComponent(result.token)
+ '&redirect=' + encodeURIComponent(options.redirect))
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
assert(res.body.error);
done();
});
}, done);
});
it('Report error for invalid token during verification', function (done) {
testConfirm(function (result, done) {
request(app)
.get('/users/confirm?uid=' + result.uid
+ '&token=' + encodeURIComponent(result.token) + '_invalid'
+ '&redirect=' + encodeURIComponent(options.redirect))
.expect(400)
.end(function (err, res) {
if (err) return done(err);
assert(res.body.error);
done();
});
}, done);
});
});
});