Merge branch 'master' into 2.0

This commit is contained in:
Miroslav Bajtoš 2014-05-28 18:41:36 +02:00
commit 18fd61a546
12 changed files with 343 additions and 57 deletions

View File

@ -19,7 +19,7 @@ For more details, see http://loopback.io/.
In addition to the [main LoopBack module](https://github.com/strongloop/loopback), LoopBack consists of numerous other modules that implement specific functionality, In addition to the [main LoopBack module](https://github.com/strongloop/loopback), LoopBack consists of numerous other modules that implement specific functionality,
as illustrated below: as illustrated below:
![LoopBack modules](./docs/assets/lb-modules.png "LoopBack modules") ![LoopBack modules](https://github.com/strongloop/loopback/raw/master/docs/assets/lb-modules.png "LoopBack modules")
* Frameworks * Frameworks
* [loopback](https://github.com/strongloop/loopback) * [loopback](https://github.com/strongloop/loopback)
@ -30,6 +30,7 @@ as illustrated below:
* [loopback-connector-mongodb](https://github.com/strongloop/loopback-connector-mongodb) * [loopback-connector-mongodb](https://github.com/strongloop/loopback-connector-mongodb)
* [loopback-connector-mysql](https://github.com/strongloop/loopback-connector-mysql) * [loopback-connector-mysql](https://github.com/strongloop/loopback-connector-mysql)
* [loopback-connector-oracle](https://github.com/strongloop/loopback-connector-oracle) * [loopback-connector-oracle](https://github.com/strongloop/loopback-connector-oracle)
* [loopback-connector-mssql](https://github.com/strongloop/loopback-connector-mssql)
* [loopback-connector-postgresql](https://github.com/strongloop/loopback-connector-postgresql) * [loopback-connector-postgresql](https://github.com/strongloop/loopback-connector-postgresql)
* [loopback-connector-rest](https://github.com/strongloop/loopback-connector-rest) * [loopback-connector-rest](https://github.com/strongloop/loopback-connector-rest)
* [loopback-connector-soap](https://github.com/strongloop/loopback-connector-soap) * [loopback-connector-soap](https://github.com/strongloop/loopback-connector-soap)
@ -57,7 +58,7 @@ as illustrated below:
* [loopback-example-access-control](https://github.com/strongloop/loopback-example-access-control) * [loopback-example-access-control](https://github.com/strongloop/loopback-example-access-control)
* [loopback-example-proxy](https://github.com/strongloop/loopback-example-proxy) * [loopback-example-proxy](https://github.com/strongloop/loopback-example-proxy)
* [strongloop-community/loopback-example-datagraph](https://github.com/strongloop-community/loopback-example-datagraph) * [strongloop-community/loopback-example-datagraph](https://github.com/strongloop-community/loopback-example-datagraph)
* [strongloop-community/loopback-mysql-example](https://github.com/strongloop-community/loopback-mysql-example) * [strongloop-community/loopback-example-database](https://github.com/strongloop-community/loopback-example-database)
* [strongloop-community/loopback-examples-ios](https://github.com/strongloop-community/loopback-examples-ios) * [strongloop-community/loopback-examples-ios](https://github.com/strongloop-community/loopback-examples-ios)
* [strongloop-community/loopback-example-ssl](https://github.com/strongloop-community/loopback-example-ssl) * [strongloop-community/loopback-example-ssl](https://github.com/strongloop-community/loopback-example-ssl)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 601 KiB

View File

@ -7,6 +7,7 @@ var DataSource = require('loopback-datasource-juggler').DataSource
, compat = require('./compat') , compat = require('./compat')
, assert = require('assert') , assert = require('assert')
, fs = require('fs') , fs = require('fs')
, extend = require('util')._extend
, _ = require('underscore') , _ = require('underscore')
, RemoteObjects = require('strong-remoting') , RemoteObjects = require('strong-remoting')
, swagger = require('strong-remoting/ext/swagger') , swagger = require('strong-remoting/ext/swagger')
@ -197,9 +198,30 @@ app.models = function () {
app.dataSource = function (name, config) { app.dataSource = function (name, config) {
this.dataSources[name] = this.dataSources[name] =
this.dataSources[classify(name)] = this.dataSources[classify(name)] =
this.dataSources[camelize(name)] = dataSourcesFromConfig(config); this.dataSources[camelize(name)] =
dataSourcesFromConfig(config, this.connectors);
} }
/**
* Register a connector.
*
* When a new data-source is being added via `app.dataSource`, the connector
* name is looked up in the registered connectors first.
*
* Connectors are required to be explicitly registered only for applications
* using browserify, because browserify does not support dynamic require,
* which is used by LoopBack to automatically load the connector module.
*
* @param {String} name Name of the connector, e.g. 'mysql'.
* @param {Object} connector Connector object as returned
* by `require('loopback-connector-{name}')`.
*/
app.connector = function(name, connector) {
this.connectors[name] =
this.connectors[classify(name)] =
this.connectors[camelize(name)] = connector;
};
/** /**
* Get all remote objects. * Get all remote objects.
* @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). * @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
@ -451,10 +473,10 @@ app.boot = function(options) {
app.set('port', appConfig.port); app.set('port', appConfig.port);
} }
assert(appConfig.restApiRoot !== undefined, 'app.restBasePath is required'); assert(appConfig.restApiRoot !== undefined, 'app.restApiRoot is required');
assert(typeof appConfig.restApiRoot === 'string', 'app.restBasePath must be a string'); assert(typeof appConfig.restApiRoot === 'string', 'app.restApiRoot must be a string');
assert(/^\//.test(appConfig.restApiRoot), 'app.restBasePath must start with "/"'); assert(/^\//.test(appConfig.restApiRoot), 'app.restApiRoot must start with "/"');
app.set('restApiRoot', appConfig.restBasePath); app.set('restApiRoot', appConfig.restApiRoot);
for(var configKey in appConfig) { for(var configKey in appConfig) {
var cur = app.get(configKey); var cur = app.get(configKey);
@ -519,25 +541,33 @@ function camelize(str) {
return stringUtils.camelize(str); return stringUtils.camelize(str);
} }
function dataSourcesFromConfig(config) { function dataSourcesFromConfig(config, connectorRegistry) {
var connectorPath; var connectorPath;
assert(typeof config === 'object', assert(typeof config === 'object',
'cannont create data source without config object'); 'cannont create data source without config object');
if(typeof config.connector === 'string') { if(typeof config.connector === 'string') {
connectorPath = path.join(__dirname, 'connectors', config.connector+'.js'); var name = config.connector;
if (connectorRegistry[name]) {
config.connector = connectorRegistry[name];
} else {
connectorPath = path.join(__dirname, 'connectors', name + '.js');
if(fs.existsSync(connectorPath)) { if (fs.existsSync(connectorPath)) {
config.connector = require(connectorPath); config.connector = require(connectorPath);
} }
} }
}
return require('./loopback').createDataSource(config); return require('./loopback').createDataSource(config);
} }
function modelFromConfig(name, config, app) { function modelFromConfig(name, config, app) {
var ModelCtor = require('./loopback').createModel(name, config.properties, config.options); var options = buildModelOptionsFromConfig(config);
var properties = config.properties;
var ModelCtor = require('./loopback').createModel(name, properties, options);
var dataSource = config.dataSource; var dataSource = config.dataSource;
if(typeof dataSource === 'string') { if(typeof dataSource === 'string') {
@ -550,6 +580,26 @@ function modelFromConfig(name, config, app) {
return ModelCtor; return ModelCtor;
} }
function buildModelOptionsFromConfig(config) {
var options = extend({}, config.options);
for (var key in config) {
if (['properties', 'options', 'dataSource'].indexOf(key) !== -1) {
// Skip items which have special meaning
continue;
}
if (options[key] !== undefined) {
// When both `config.key` and `config.options.key` are set,
// use the latter one to preserve backwards compatibility
// with loopback 1.x
continue;
}
options[key] = config[key];
}
return options;
}
function requireDir(dir, basenames) { function requireDir(dir, basenames) {
assert(dir, 'cannot require directory contents without directory name'); assert(dir, 'cannot require directory contents without directory name');

View File

@ -76,6 +76,18 @@ function createApplication() {
return proto.models.apply(this, arguments); return proto.models.apply(this, arguments);
}; };
// Create a new instance of datasources registry per each app instance
app.datasources = app.dataSources = {};
// Create a new instance of connector registry per each app instance
app.connectors = {};
// Register built-in connectors. It's important to keep this code
// hand-written, so that all require() calls are static
// and thus browserify can process them (include connectors in the bundle)
app.connector('memory', loopback.Memory);
app.connector('remote', loopback.Remote);
return app; return app;
} }

View File

@ -15,6 +15,7 @@ module.exports = rest;
*/ */
function rest() { function rest() {
var tokenParser = null;
return function (req, res, next) { return function (req, res, next) {
var app = req.app; var app = req.app;
var handler = app.handler('rest'); var handler = app.handler('rest');
@ -23,9 +24,28 @@ function rest() {
res.send(handler.adapter.allRoutes()); res.send(handler.adapter.allRoutes());
} else if(req.url === '/models') { } else if(req.url === '/models') {
return res.send(app.remotes().toJSON()); return res.send(app.remotes().toJSON());
} else if (app.isAuthEnabled) {
if (!tokenParser) {
// NOTE(bajtos) It would be better to search app.models for a model
// of type AccessToken instead of searching all loopback models.
// Unfortunately that's not supported now.
// Related discussions:
// https://github.com/strongloop/loopback/pull/167
// https://github.com/strongloop/loopback/commit/f07446a
var AccessToken = loopback.getModelByType(loopback.AccessToken);
tokenParser = loopback.token({ model: AccessToken });
}
tokenParser(req, res, function(err) {
if (err) {
next(err);
} else { } else {
handler(req, res, next); handler(req, res, next);
} }
});
} else {
handler(req, res, next);
} }
};
} }

View File

@ -53,12 +53,14 @@ function token(options) {
assert(TokenModel, 'loopback.token() middleware requires a AccessToken model'); assert(TokenModel, 'loopback.token() middleware requires a AccessToken model');
return function (req, res, next) { return function (req, res, next) {
if (req.accessToken !== undefined) return next();
TokenModel.findForRequest(req, options, function(err, token) { TokenModel.findForRequest(req, options, function(err, token) {
if(err) return next(err); if(err) return next(err);
if(token) { if(token) {
req.accessToken = token; req.accessToken = token;
next(); next();
} else { } else {
req.accessToken = null;
return next(); return next();
} }
}); });

View File

@ -1,13 +1,29 @@
{ {
"name": "loopback", "name": "loopback",
"description": "LoopBack: Open Mobile Platform for Node.js", "description": "LoopBack: Open API Framework for Node.js",
"homepage": "http://loopback.io", "homepage": "http://loopback.io",
"keywords": [ "keywords": [
"restful",
"rest",
"api",
"express",
"restify",
"koa",
"auth",
"security",
"oracle",
"mysql",
"nosql",
"mongo",
"mongodb",
"sqlserver",
"mssql",
"postgres",
"postgresql",
"soap",
"StrongLoop", "StrongLoop",
"LoopBack", "framework",
"Mobile", "mobile",
"Backend",
"Platform",
"mBaaS" "mBaaS"
], ],
"version": "2.0.0-beta2", "version": "2.0.0-beta2",
@ -15,19 +31,19 @@
"test": "mocha -R spec" "test": "mocha -R spec"
}, },
"dependencies": { "dependencies": {
"debug": "~0.7.4", "debug": "~0.8.1",
"express": "~3.4.8", "express": "~3.5.0",
"strong-remoting": "~1.4.0", "strong-remoting": "~1.4.0",
"inflection": "~1.3.5", "inflection": "~1.3.5",
"passport": "~0.2.0", "passport": "~0.2.0",
"passport-local": "~0.1.6", "passport-local": "~1.0.0",
"nodemailer": "~0.6.0", "nodemailer": "~0.6.5",
"ejs": "~0.8.5", "ejs": "~1.0.0",
"bcryptjs": "~0.7.12", "bcryptjs": "~0.7.12",
"underscore.string": "~2.3.3", "underscore.string": "~2.3.3",
"underscore": "~1.6.0", "underscore": "~1.6.0",
"uid2": "0.0.3", "uid2": "0.0.3",
"async": "~0.2.10", "async": "~0.9.0",
"canonical-json": "0.0.3" "canonical-json": "0.0.3"
}, },
"peerDependencies": { "peerDependencies": {
@ -35,26 +51,26 @@
}, },
"devDependencies": { "devDependencies": {
"loopback-datasource-juggler": "2.0.0-beta1", "loopback-datasource-juggler": "2.0.0-beta1",
"mocha": "~1.17.1", "mocha": "~1.18.0",
"strong-task-emitter": "0.0.x", "strong-task-emitter": "0.0.x",
"supertest": "~0.9.0", "supertest": "~0.12.1",
"chai": "~1.9.0", "chai": "~1.9.1",
"loopback-testing": "~0.1.2", "loopback-testing": "~0.1.2",
"browserify": "~3.41.0", "browserify": "~4.1.5",
"grunt": "~0.4.2", "grunt": "~0.4.5",
"grunt-browserify": "~1.3.1", "grunt-browserify": "~2.1.0",
"grunt-contrib-uglify": "~0.3.2", "grunt-contrib-uglify": "~0.4.0",
"grunt-contrib-jshint": "~0.8.0", "grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-watch": "~0.5.3", "grunt-contrib-watch": "~0.6.1",
"karma-script-launcher": "~0.1.0", "karma-script-launcher": "~0.1.0",
"karma-chrome-launcher": "~0.1.2", "karma-chrome-launcher": "~0.1.3",
"karma-firefox-launcher": "~0.1.3", "karma-firefox-launcher": "~0.1.3",
"karma-html2js-preprocessor": "~0.1.0", "karma-html2js-preprocessor": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.2", "karma-phantomjs-launcher": "~0.1.4",
"karma": "~0.10.9", "karma": "~0.12.16",
"karma-browserify": "~0.2.0", "karma-browserify": "~0.2.0",
"karma-mocha": "~0.1.1", "karma-mocha": "~0.1.3",
"grunt-karma": "~0.6.2", "grunt-karma": "~0.8.3"
"loopback-explorer": "~1.1.0" "loopback-explorer": "~1.1.0"
}, },
"repository": { "repository": {

View File

@ -32,6 +32,27 @@ describe('loopback.token(options)', function() {
.end(done); .end(done);
}); });
}); });
it('should skip when req.token is already present', function(done) {
var tokenStub = { id: 'stub id' };
app.use(function(req, res, next) {
req.accessToken = tokenStub;
next();
});
app.use(loopback.token({ model: Token }));
app.get('/', function(req, res, next) {
res.send(req.accessToken);
});
request(app).get('/')
.set('Authorization', this.token.id)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.eql(tokenStub);
done();
});
});
}); });
describe('AccessToken', function () { describe('AccessToken', function () {

View File

@ -14,8 +14,12 @@ function checkResult(err, result) {
assert(!err); assert(!err);
} }
describe('security scopes', function () { var ds = null;
before(function() {
ds = loopback.createDataSource({connector: loopback.Memory});
});
describe('security scopes', function () {
beforeEach(function() { beforeEach(function() {
var ds = this.ds = loopback.createDataSource({connector: loopback.Memory}); var ds = this.ds = loopback.createDataSource({connector: loopback.Memory});
testModel = loopback.DataModel.extend('testModel'); testModel = loopback.DataModel.extend('testModel');
@ -156,7 +160,6 @@ describe('security ACLs', function () {
}); });
it("should honor defaultPermission from the model", function () { it("should honor defaultPermission from the model", function () {
var ds = this.ds;
var Customer = ds.createModel('Customer', { var Customer = ds.createModel('Customer', {
name: { name: {
type: String, type: String,
@ -188,7 +191,6 @@ describe('security ACLs', function () {
}); });
it("should honor static ACLs from the model", function () { it("should honor static ACLs from the model", function () {
var ds = this.ds;
var Customer = ds.createModel('Customer', { var Customer = ds.createModel('Customer', {
name: { name: {
type: String, type: String,
@ -226,7 +228,6 @@ describe('security ACLs', function () {
it("should check access against LDL, ACL, and Role", function () { it("should check access against LDL, ACL, and Role", function () {
// var log = console.log; // var log = console.log;
var log = function() {}; var log = function() {};
var ds = this.ds;
// Create // Create
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {

View File

@ -69,9 +69,11 @@ describe('app', function() {
}); });
}); });
describe('app.model(name, properties, options)', function () { describe('app.model(name, config)', function () {
it('Sugar for defining a fully built model', function () { var app;
var app = loopback();
beforeEach(function() {
app = loopback();
app.boot({ app.boot({
app: {port: 3000, host: '127.0.0.1'}, app: {port: 3000, host: '127.0.0.1'},
dataSources: { dataSources: {
@ -80,20 +82,44 @@ describe('app', function() {
} }
} }
}); });
});
it('Sugar for defining a fully built model', function () {
app.model('foo', { app.model('foo', {
dataSource: 'db' dataSource: 'db'
}); });
var Foo = app.models.foo; var Foo = app.models.foo;
var f = new Foo; var f = new Foo();
assert(f instanceof loopback.Model); assert(f instanceof loopback.Model);
}); });
it('interprets extra first-level keys as options', function() {
app.model('foo', {
dataSource: 'db',
base: 'User'
});
expect(app.models.foo.definition.settings.base).to.equal('User');
});
it('prefers config.options.key over config.key', function() {
app.model('foo', {
dataSource: 'db',
base: 'User',
options: {
base: 'Application'
}
});
expect(app.models.foo.definition.settings.base).to.equal('Application');
});
}); });
describe('app.models', function() { describe('app.models', function() {
it('is unique per app instance', function() { it('is unique per app instance', function() {
app.dataSource('db', { connector: 'memory' });
var Color = app.model('Color', { dataSource: 'db' }); var Color = app.model('Color', { dataSource: 'db' });
expect(app.models.Color).to.equal(Color); expect(app.models.Color).to.equal(Color);
var anotherApp = loopback(); var anotherApp = loopback();
@ -101,6 +127,23 @@ describe('app', function() {
}); });
}); });
describe('app.dataSources', function() {
it('is unique per app instance', function() {
app.dataSource('ds', { connector: 'memory' });
expect(app.datasources.ds).to.not.equal(undefined);
var anotherApp = loopback();
expect(anotherApp.datasources.ds).to.equal(undefined);
});
});
describe('app.dataSource', function() {
it('looks up the connector in `app.connectors`', function() {
app.connector('custom', loopback.Memory);
app.dataSource('custom', { connector: 'custom' });
expect(app.dataSources.custom.name).to.equal(loopback.Memory.name);
});
});
describe('app.boot([options])', function () { describe('app.boot([options])', function () {
beforeEach(function () { beforeEach(function () {
app.boot({ app.boot({
@ -475,4 +518,39 @@ describe('app', function() {
}); });
}); });
}); });
describe('app.connectors', function() {
it('is unique per app instance', function() {
app.connectors.foo = 'bar';
var anotherApp = loopback();
expect(anotherApp.connectors.foo).to.equal(undefined);
});
it('includes Remote connector', function() {
expect(app.connectors.remote).to.equal(loopback.Remote);
});
it('includes Memory connector', function() {
expect(app.connectors.memory).to.equal(loopback.Memory);
});
});
describe('app.connector', function() {
// any connector will do
it('adds the connector to the registry', function() {
app.connector('foo-bar', loopback.Memory);
expect(app.connectors['foo-bar']).to.equal(loopback.Memory);
});
it('adds a classified alias', function() {
app.connector('foo-bar', loopback.Memory);
expect(app.connectors.FooBar).to.equal(loopback.Memory);
});
it('adds a camelized alias', function() {
app.connector('FOO-BAR', loopback.Memory);
console.log(app.connectors);
expect(app.connectors.FOOBAR).to.equal(loopback.Memory);
});
});
}); });

View File

@ -0,0 +1,86 @@
describe('loopback.rest', function() {
beforeEach(function() {
app.dataSource('db', { connector: loopback.Memory });
});
it('works out-of-the-box', function(done) {
app.model('MyModel', { dataSource: 'db' });
app.use(loopback.rest());
request(app).get('/mymodels')
.expect(200)
.end(done);
});
it('includes loopback.token when necessary', function(done) {
givenUserModelWithAuth();
app.enableAuth();
app.use(loopback.rest());
givenLoggedInUser(function(err, token) {
if (err) return done(err);
expect(token).instanceOf(app.models.accessToken);
request(app).get('/users/' + token.userId)
.set('Authorization', token.id)
.expect(200)
.end(done);
});
});
it('does not include loopback.token when auth not enabled', function(done) {
var User = givenUserModelWithAuth();
User.getToken = function(req, cb) {
cb(null, req.accessToken ? req.accessToken.id : null);
};
loopback.remoteMethod(User.getToken, {
accepts: [{ type: 'object', http: { source: 'req' } }],
returns: [{ type: 'object', name: 'id' }]
});
app.use(loopback.rest());
givenLoggedInUser(function(err, token) {
if (err) return done(err);
request(app).get('/users/getToken')
.set('Authorization', token.id)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
expect(res.body.id).to.equal(null);
done();
});
});
});
function givenUserModelWithAuth() {
// NOTE(bajtos) It is important to create a custom AccessToken model here,
// in order to overwrite the entry created by previous tests in
// the global model registry
app.model('accessToken', {
options: {
base: 'AccessToken'
},
dataSource: 'db'
});
return app.model('user', {
options: {
base: 'User',
relations: {
accessTokens: {
model: 'accessToken',
type: 'hasMany',
foreignKey: 'userId'
}
}
},
dataSource: 'db'
});
}
function givenLoggedInUser(cb) {
var credentials = { email: 'user@example.com', password: 'pwd' };
var User = app.models.user;
User.create(credentials,
function(err, user) {
if (err) return done(err);
User.login(credentials, cb);
});
}
});

View File

@ -51,16 +51,15 @@ describe('User', function(){
it('Email is required', function (done) { it('Email is required', function (done) {
User.create({password: '123'}, function (err) { User.create({password: '123'}, function (err) {
assert.deepEqual(err, {name: "ValidationError", assert(err);
message: "The Model instance is not valid. See `details` " assert.equal(err.name, "ValidationError");
+ "property of the error object for more info.", assert.equal(err.statusCode, 422);
statusCode: 422, assert.equal(err.details.context, "user");
details: { assert.deepEqual(err.details.codes.email, [
context: "user", 'presence',
codes: {email: ["presence", "format.blank", "uniqueness"]}, 'format.blank',
messages: {email: ["can't be blank", "is blank", 'uniqueness'
"Email already exists"]}}} ]);
);
done(); done();
}); });