Merge branch 'release/2.8.0' into production

This commit is contained in:
Raymond Feng 2014-11-19 11:38:58 -08:00
commit 5526402e1c
44 changed files with 4533 additions and 938 deletions

22
.jscsrc Normal file
View File

@ -0,0 +1,22 @@
{
"preset": "google",
"requireCurlyBraces": [
"else",
"for",
"while",
"do",
"try",
"catch"
],
"disallowSpacesInsideObjectBrackets": null,
"maximumLineLength": {
"value": 150,
"allowComments": true,
"allowRegex": true
},
"validateJSDoc": {
"checkParamNames": false,
"checkRedundantParams": false,
"requireParamTypes": true
}
}

View File

@ -5,10 +5,9 @@
"indent": 2, "indent": 2,
"undef": true, "undef": true,
"quotmark": "single", "quotmark": "single",
"maxlen": 150,
"trailing": true,
"newcap": true, "newcap": true,
"nonew": true, "nonew": true,
"sub": true,
"laxcomma": true, "laxcomma": true,
"laxbreak": true "laxbreak": true
} }

2301
CHANGES.md

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ Contributing to `loopback` is easy. In a few simple steps:
* Adhere to code style outlined in the [Google C++ Style Guide][] and * Adhere to code style outlined in the [Google C++ Style Guide][] and
[Google Javascript Style Guide][]. [Google Javascript Style Guide][].
* Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback) * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback)
* Submit a pull request through Github. * Submit a pull request through Github.

View File

@ -33,9 +33,23 @@ module.exports = function(grunt) {
lib: { lib: {
src: ['lib/**/*.js'] src: ['lib/**/*.js']
}, },
test: { common: {
src: ['test/**/*.js'] src: ['common/**/*.js']
},
server: {
src: ['server/**/*.js']
} }
// TODO tests don't pass yet
// test: {
// src: ['test/**/*.js']
// }
},
jscs: {
gruntfile: 'Gruntfile.js',
lib: ['lib/**/*.js'],
common: ['common/**/*.js'],
server: ['server/**/*.js']
// TODO(bajtos) - test/**/*.js
}, },
watch: { watch: {
gruntfile: { gruntfile: {
@ -80,7 +94,7 @@ module.exports = function(grunt) {
karma: { karma: {
'unit-once': { 'unit-once': {
configFile: 'test/karma.conf.js', configFile: 'test/karma.conf.js',
browsers: [ 'PhantomJS' ], browsers: ['PhantomJS'],
singleRun: true, singleRun: true,
reporters: ['dots', 'junit'], reporters: ['dots', 'junit'],
@ -182,6 +196,7 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-jscs');
grunt.loadNpmTasks('grunt-karma'); grunt.loadNpmTasks('grunt-karma');
grunt.registerTask('e2e-server', function() { grunt.registerTask('e2e-server', function() {
@ -196,6 +211,8 @@ module.exports = function(grunt) {
grunt.registerTask('default', ['browserify']); grunt.registerTask('default', ['browserify']);
grunt.registerTask('test', [ grunt.registerTask('test', [
'jscs',
'jshint',
process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit', process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit',
'karma:unit-once']); 'karma:unit-once']);

View File

@ -2,10 +2,10 @@
* Module Dependencies. * Module Dependencies.
*/ */
var loopback = require('../../lib/loopback') var loopback = require('../../lib/loopback');
, assert = require('assert') var assert = require('assert');
, uid = require('uid2') var uid = require('uid2');
, DEFAULT_TOKEN_LEN = 64; var DEFAULT_TOKEN_LEN = 64;
/** /**
* Token based authentication and access control. * Token based authentication and access control.
@ -57,7 +57,7 @@ module.exports = function(AccessToken) {
fn(null, guid); fn(null, guid);
} }
}); });
} };
/*! /*!
* Hook to create accessToken id. * Hook to create accessToken id.
@ -75,7 +75,7 @@ module.exports = function(AccessToken) {
next(); next();
} }
}); });
} };
/** /**
* Find a token for the given `ServerRequest`. * Find a token for the given `ServerRequest`.
@ -88,6 +88,11 @@ module.exports = function(AccessToken) {
*/ */
AccessToken.findForRequest = function(req, options, cb) { AccessToken.findForRequest = function(req, options, cb) {
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
var id = tokenIdForRequest(req, options); var id = tokenIdForRequest(req, options);
if (id) { if (id) {
@ -115,7 +120,7 @@ module.exports = function(AccessToken) {
cb(); cb();
}); });
} }
} };
/** /**
* Validate the token. * Validate the token.
@ -151,7 +156,7 @@ module.exports = function(AccessToken) {
} catch (e) { } catch (e) {
cb(e); cb(e);
} }
} };
function tokenIdForRequest(req, options) { function tokenIdForRequest(req, options) {
var params = options.params || []; var params = options.params || [];

View File

@ -179,7 +179,7 @@ module.exports = function(ACL) {
ACL.prototype.score = function(req) { ACL.prototype.score = function(req) {
return this.constructor.getMatchingScore(this, req); return this.constructor.getMatchingScore(this, req);
} };
/*! /*!
* Resolve permission from the ACLs * Resolve permission from the ACLs
@ -199,24 +199,26 @@ module.exports = function(ACL) {
var score = 0; var score = 0;
for (var i = 0; i < acls.length; i++) { for (var i = 0; i < acls.length; i++) {
score = ACL.getMatchingScore(acls[i], req); var candidate = acls[i];
score = ACL.getMatchingScore(candidate, req);
if (score < 0) { if (score < 0) {
// the highest scored ACL did not match // the highest scored ACL did not match
break; break;
} }
if (!req.isWildcard()) { if (!req.isWildcard()) {
// We should stop from the first match for non-wildcard // We should stop from the first match for non-wildcard
permission = acls[i].permission; permission = candidate.permission;
break; break;
} else { } else {
if (req.exactlyMatches(acls[i])) { if (req.exactlyMatches(candidate)) {
permission = acls[i].permission; permission = candidate.permission;
break; break;
} }
// For wildcard match, find the strongest permission // For wildcard match, find the strongest permission
if (AccessContext.permissionOrder[acls[i].permission] var candidateOrder = AccessContext.permissionOrder[candidate.permission];
> AccessContext.permissionOrder[permission]) { var permissionOrder = AccessContext.permissionOrder[permission];
permission = acls[i].permission; if (candidateOrder > permissionOrder) {
permission = candidate.permission;
} }
} }
} }
@ -246,8 +248,7 @@ module.exports = function(ACL) {
var staticACLs = []; var staticACLs = [];
if (modelClass && modelClass.settings.acls) { if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function(acl) { modelClass.settings.acls.forEach(function(acl) {
if (!acl.property || acl.property === ACL.ALL if (!acl.property || acl.property === ACL.ALL || property === acl.property) {
|| property === acl.property) {
staticACLs.push(new ACL({ staticACLs.push(new ACL({
model: model, model: model,
property: acl.property || ACL.ALL, property: acl.property || ACL.ALL,
@ -259,11 +260,15 @@ module.exports = function(ACL) {
} }
}); });
} }
var prop = modelClass && var prop = modelClass && (
(modelClass.definition.properties[property] // regular property // regular property
|| (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope modelClass.definition.properties[property] ||
|| modelClass[property] // static method // relation/scope
|| modelClass.prototype[property]); // prototype method (modelClass._scopeMeta && modelClass._scopeMeta[property]) ||
// static method
modelClass[property] ||
// prototype method
modelClass.prototype[property]);
if (prop && prop.acls) { if (prop && prop.acls) {
prop.acls.forEach(function(acl) { prop.acls.forEach(function(acl) {
staticACLs.push(new ACL({ staticACLs.push(new ACL({
@ -311,7 +316,7 @@ module.exports = function(ACL) {
debug('Permission denied by statically resolved permission'); debug('Permission denied by statically resolved permission');
debug(' Resolved Permission: %j', resolved); debug(' Resolved Permission: %j', resolved);
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, resolved); if (callback) callback(null, resolved);
}); });
return; return;
} }
@ -321,7 +326,7 @@ module.exports = function(ACL) {
model: model, property: propertyQuery, accessType: accessTypeQuery}}, model: model, property: propertyQuery, accessType: accessTypeQuery}},
function(err, dynACLs) { function(err, dynACLs) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
acls = acls.concat(dynACLs); acls = acls.concat(dynACLs);
@ -330,7 +335,7 @@ module.exports = function(ACL) {
var modelClass = loopback.findModel(model); var modelClass = loopback.findModel(model);
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
} }
callback && callback(null, resolved); if (callback) callback(null, resolved);
}); });
}; };
@ -344,7 +349,7 @@ module.exports = function(ACL) {
debug('accessType %s', this.accessType); debug('accessType %s', this.accessType);
debug('permission %s', this.permission); debug('permission %s', this.permission);
} }
} };
/** /**
* Check if the request has the permission to access. * Check if the request has the permission to access.
@ -381,7 +386,7 @@ module.exports = function(ACL) {
this.find({where: {model: model.modelName, property: propertyQuery, this.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function(err, acls) { accessType: accessTypeQuery}}, function(err, acls) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
var inRoleTasks = []; var inRoleTasks = [];
@ -392,8 +397,9 @@ module.exports = function(ACL) {
// Check exact matches // Check exact matches
for (var i = 0; i < context.principals.length; i++) { for (var i = 0; i < context.principals.length; i++) {
var p = context.principals[i]; var p = context.principals[i];
if (p.type === acl.principalType var typeMatch = p.type === acl.principalType;
&& String(p.id) === String(acl.principalId)) { var idMatch = String(p.id) === String(acl.principalId);
if (typeMatch && idMatch) {
effectiveACLs.push(acl); effectiveACLs.push(acl);
return; return;
} }
@ -415,7 +421,7 @@ module.exports = function(ACL) {
async.parallel(inRoleTasks, function(err, results) { async.parallel(inRoleTasks, function(err, results) {
if (err) { if (err) {
callback && callback(err, null); if (callback) callback(err, null);
return; return;
} }
var resolved = self.resolvePermission(effectiveACLs, req); var resolved = self.resolvePermission(effectiveACLs, req);
@ -424,7 +430,7 @@ module.exports = function(ACL) {
} }
debug('---Resolved---'); debug('---Resolved---');
resolved.debug(); resolved.debug();
callback && callback(null, resolved); if (callback) callback(null, resolved);
}); });
}); });
}; };
@ -452,11 +458,10 @@ module.exports = function(ACL) {
this.checkAccessForContext(context, function(err, access) { this.checkAccessForContext(context, function(err, access) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
callback && callback(null, access.permission !== ACL.DENY); if (callback) callback(null, access.permission !== ACL.DENY);
}); });
}; };
};
}

View File

@ -141,7 +141,7 @@ module.exports = function(Application) {
Application.resetKeys = function(appId, cb) { Application.resetKeys = function(appId, cb) {
this.findById(appId, function(err, app) { this.findById(appId, function(err, app) {
if (err) { if (err) {
cb && cb(err, app); if (cb) cb(err, app);
return; return;
} }
app.resetKeys(cb); app.resetKeys(cb);
@ -166,7 +166,7 @@ module.exports = function(Application) {
Application.authenticate = function(appId, key, cb) { Application.authenticate = function(appId, key, cb) {
this.findById(appId, function(err, app) { this.findById(appId, function(err, app) {
if (err || !app) { if (err || !app) {
cb && cb(err, null); if (cb) cb(err, null);
return; return;
} }
var result = null; var result = null;
@ -180,7 +180,7 @@ module.exports = function(Application) {
break; break;
} }
} }
cb && cb(null, result); if (cb) cb(null, result);
}); });
}; };
}; };

View File

@ -2,14 +2,13 @@
* Module Dependencies. * Module Dependencies.
*/ */
var PersistedModel = require('../../lib/loopback').PersistedModel var PersistedModel = require('../../lib/loopback').PersistedModel;
, loopback = require('../../lib/loopback') var loopback = require('../../lib/loopback');
, crypto = require('crypto') var crypto = require('crypto');
, CJSON = {stringify: require('canonical-json')} var CJSON = {stringify: require('canonical-json')};
, async = require('async') var async = require('async');
, assert = require('assert') var assert = require('assert');
, debug = require('debug')('loopback:change'); var debug = require('debug')('loopback:change');
/** /**
* Change list entry. * Change list entry.
@ -55,8 +54,8 @@ module.exports = function(Change) {
if (!hasModel) return null; if (!hasModel) return null;
return Change.idForModel(this.modelName, this.modelId); return Change.idForModel(this.modelName, this.modelId);
} };
} };
Change.setup(); Change.setup();
/** /**
@ -82,7 +81,7 @@ module.exports = function(Change) {
}); });
}); });
async.parallel(tasks, callback); async.parallel(tasks, callback);
} };
/** /**
* Get an identifier for a given model. * Get an identifier for a given model.
@ -94,7 +93,7 @@ module.exports = function(Change) {
Change.idForModel = function(modelName, modelId) { Change.idForModel = function(modelName, modelId) {
return this.hash([modelName, modelId].join('-')); return this.hash([modelName, modelId].join('-'));
} };
/** /**
* Find or create a change for the given model. * Find or create a change for the given model.
@ -126,7 +125,7 @@ module.exports = function(Change) {
ch.save(callback); ch.save(callback);
} }
}); });
} };
/** /**
* Update (or create) the change with the current revision. * Update (or create) the change with the current revision.
@ -148,7 +147,7 @@ module.exports = function(Change) {
cb = cb || function(err) { cb = cb || function(err) {
if (err) throw new Error(err); if (err) throw new Error(err);
} };
async.parallel(tasks, function(err) { async.parallel(tasks, function(err) {
if (err) return cb(err); if (err) return cb(err);
@ -194,7 +193,7 @@ module.exports = function(Change) {
cb(); cb();
}); });
} }
} };
/** /**
* Get a change's current revision based on current data. * Get a change's current revision based on current data.
@ -214,7 +213,7 @@ module.exports = function(Change) {
cb(null, null); cb(null, null);
} }
}); });
} };
/** /**
* Create a hash of the given `string` with the `options.hashAlgorithm`. * Create a hash of the given `string` with the `options.hashAlgorithm`.
@ -229,7 +228,7 @@ module.exports = function(Change) {
.createHash(Change.settings.hashAlgorithm || 'sha1') .createHash(Change.settings.hashAlgorithm || 'sha1')
.update(str) .update(str)
.digest('hex'); .digest('hex');
} };
/** /**
* Get the revision string for the given object * Get the revision string for the given object
@ -239,7 +238,7 @@ module.exports = function(Change) {
Change.revisionForInst = function(inst) { Change.revisionForInst = function(inst) {
return this.hash(CJSON.stringify(inst)); return this.hash(CJSON.stringify(inst));
} };
/** /**
* Get a change's type. Returns one of: * Get a change's type. Returns one of:
@ -263,7 +262,7 @@ module.exports = function(Change) {
return Change.DELETE; return Change.DELETE;
} }
return Change.UNKNOWN; return Change.UNKNOWN;
} };
/** /**
* Compare two changes. * Compare two changes.
@ -276,7 +275,7 @@ module.exports = function(Change) {
var thisRev = this.rev || null; var thisRev = this.rev || null;
var thatRev = change.rev || null; var thatRev = change.rev || null;
return thisRev === thatRev; return thisRev === thatRev;
} };
/** /**
* Does this change conflict with the given change. * Does this change conflict with the given change.
@ -290,7 +289,7 @@ module.exports = function(Change) {
if (Change.bothDeleted(this, change)) return false; if (Change.bothDeleted(this, change)) return false;
if (this.isBasedOn(change)) return false; if (this.isBasedOn(change)) return false;
return true; return true;
} };
/** /**
* Are both changes deletes? * Are both changes deletes?
@ -300,9 +299,9 @@ module.exports = function(Change) {
*/ */
Change.bothDeleted = function(a, b) { Change.bothDeleted = function(a, b) {
return a.type() === Change.DELETE return a.type() === Change.DELETE &&
&& b.type() === Change.DELETE; b.type() === Change.DELETE;
} };
/** /**
* Determine if the change is based on the given change. * Determine if the change is based on the given change.
@ -312,7 +311,7 @@ module.exports = function(Change) {
Change.prototype.isBasedOn = function(change) { Change.prototype.isBasedOn = function(change) {
return this.prev === change.rev; return this.prev === change.rev;
} };
/** /**
* Determine the differences for a given model since a given checkpoint. * Determine the differences for a given model since a given checkpoint.
@ -393,11 +392,11 @@ module.exports = function(Change) {
conflicts: conflicts conflicts: conflicts
}); });
}); });
} };
/** /**
* Correct all change list entries. * Correct all change list entries.
* @param {Function} callback * @param {Function} cb
*/ */
Change.rectifyAll = function(cb) { Change.rectifyAll = function(cb) {
@ -407,11 +406,10 @@ module.exports = function(Change) {
this.find(function(err, changes) { this.find(function(err, changes) {
if (err) return cb(err); if (err) return cb(err);
changes.forEach(function(change) { changes.forEach(function(change) {
change = new Change(change);
change.rectify(); change.rectify();
}); });
}); });
} };
/** /**
* Get the checkpoint model. * Get the checkpoint model.
@ -426,13 +424,13 @@ module.exports = function(Change) {
+ ' is not attached to a dataSource'); + ' is not attached to a dataSource');
checkpointModel.attachTo(this.dataSource); checkpointModel.attachTo(this.dataSource);
return checkpointModel; return checkpointModel;
} };
Change.handleError = function(err) { Change.handleError = function(err) {
if (!this.settings.ignoreErrors) { if (!this.settings.ignoreErrors) {
throw err; throw err;
} }
} };
Change.prototype.debug = function() { Change.prototype.debug = function() {
if (debug.enabled) { if (debug.enabled) {
@ -445,7 +443,7 @@ module.exports = function(Change) {
debug('\tmodelId', this.modelId); debug('\tmodelId', this.modelId);
debug('\ttype', this.type()); debug('\ttype', this.type());
} }
} };
/** /**
* Get the `Model` class for `change.modelName`. * Get the `Model` class for `change.modelName`.
@ -454,7 +452,7 @@ module.exports = function(Change) {
Change.prototype.getModelCtor = function() { Change.prototype.getModelCtor = function() {
return this.constructor.settings.trackModel; return this.constructor.settings.trackModel;
} };
Change.prototype.getModelId = function() { Change.prototype.getModelId = function() {
// TODO(ritch) get rid of the need to create an instance // TODO(ritch) get rid of the need to create an instance
@ -463,13 +461,13 @@ module.exports = function(Change) {
var m = new Model(); var m = new Model();
m.setId(id); m.setId(id);
return m.getId(); return m.getId();
} };
Change.prototype.getModel = function(callback) { Change.prototype.getModel = function(callback) {
var Model = this.constructor.settings.trackModel; var Model = this.constructor.settings.trackModel;
var id = this.getModelId(); var id = this.getModelId();
Model.findById(id, callback); Model.findById(id, callback);
} };
/** /**
* When two changes conflict a conflict is created. * When two changes conflict a conflict is created.
@ -533,7 +531,7 @@ module.exports = function(Change) {
if (err) return cb(err); if (err) return cb(err);
cb(null, source, target); cb(null, source, target);
} }
} };
/** /**
* Get the conflicting changes. * Get the conflicting changes.
@ -578,7 +576,7 @@ module.exports = function(Change) {
if (err) return cb(err); if (err) return cb(err);
cb(null, sourceChange, targetChange); cb(null, sourceChange, targetChange);
} }
} };
/** /**
* Resolve the conflict. * Resolve the conflict.
@ -594,7 +592,7 @@ module.exports = function(Change) {
sourceChange.prev = targetChange.rev; sourceChange.prev = targetChange.rev;
sourceChange.save(cb); sourceChange.save(cb);
}); });
} };
/** /**
* Determine the conflict type. * Determine the conflict type.
@ -624,5 +622,5 @@ module.exports = function(Change) {
} }
return cb(null, Change.UNKNOWN); return cb(null, Change.UNKNOWN);
}); });
} };
}; };

View File

@ -47,7 +47,7 @@ module.exports = function(Checkpoint) {
}); });
} }
}); });
} };
Checkpoint.beforeSave = function(next, model) { Checkpoint.beforeSave = function(next, model) {
if (!model.getId() && model.seq === undefined) { if (!model.getId() && model.seq === undefined) {
@ -59,5 +59,5 @@ module.exports = function(Checkpoint) {
} else { } else {
next(); next();
} }
} };
}; };

View File

@ -23,14 +23,14 @@ module.exports = function(RoleMapping) {
* @param {Error} err * @param {Error} err
* @param {Application} application * @param {Application} application
*/ */
RoleMapping.prototype.application = function (callback) { RoleMapping.prototype.application = function(callback) {
if (this.principalType === RoleMapping.APPLICATION) { if (this.principalType === RoleMapping.APPLICATION) {
var applicationModel = this.constructor.Application var applicationModel = this.constructor.Application ||
|| loopback.getModelByType(loopback.Application); loopback.getModelByType(loopback.Application);
applicationModel.findById(this.principalId, callback); applicationModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function () { process.nextTick(function() {
callback && callback(null, null); if (callback) callback(null, null);
}); });
} }
}; };
@ -41,14 +41,14 @@ module.exports = function(RoleMapping) {
* @param {Error} err * @param {Error} err
* @param {User} user * @param {User} user
*/ */
RoleMapping.prototype.user = function (callback) { RoleMapping.prototype.user = function(callback) {
if (this.principalType === RoleMapping.USER) { if (this.principalType === RoleMapping.USER) {
var userModel = this.constructor.User var userModel = this.constructor.User ||
|| loopback.getModelByType(loopback.User); loopback.getModelByType(loopback.User);
userModel.findById(this.principalId, callback); userModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function () { process.nextTick(function() {
callback && callback(null, null); if (callback) callback(null, null);
}); });
} }
}; };
@ -59,14 +59,14 @@ module.exports = function(RoleMapping) {
* @param {Error} err * @param {Error} err
* @param {User} childUser * @param {User} childUser
*/ */
RoleMapping.prototype.childRole = function (callback) { RoleMapping.prototype.childRole = function(callback) {
if (this.principalType === RoleMapping.ROLE) { if (this.principalType === RoleMapping.ROLE) {
var roleModel = this.constructor.Role || var roleModel = this.constructor.Role ||
loopback.getModelByType(loopback.Role); loopback.getModelByType(loopback.Role);
roleModel.findById(this.principalId, callback); roleModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function () { process.nextTick(function() {
callback && callback(null, null); if (callback) callback(null, null);
}); });
} }
}; };

View File

@ -33,7 +33,7 @@ module.exports = function(Role) {
roleMappingModel.find({where: {roleId: this.id, roleMappingModel.find({where: {roleId: this.id,
principalType: RoleMapping.USER}}, function(err, mappings) { principalType: RoleMapping.USER}}, function(err, mappings) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
return mappings.map(function(m) { return mappings.map(function(m) {
@ -46,7 +46,7 @@ module.exports = function(Role) {
roleMappingModel.find({where: {roleId: this.id, roleMappingModel.find({where: {roleId: this.id,
principalType: RoleMapping.APPLICATION}}, function(err, mappings) { principalType: RoleMapping.APPLICATION}}, function(err, mappings) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
return mappings.map(function(m) { return mappings.map(function(m) {
@ -59,7 +59,7 @@ module.exports = function(Role) {
roleMappingModel.find({where: {roleId: this.id, roleMappingModel.find({where: {roleId: this.id,
principalType: RoleMapping.ROLE}}, function(err, mappings) { principalType: RoleMapping.ROLE}}, function(err, mappings) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
return mappings.map(function(m) { return mappings.map(function(m) {
@ -72,10 +72,10 @@ module.exports = function(Role) {
// Special roles // Special roles
Role.OWNER = '$owner'; // owner of the object Role.OWNER = '$owner'; // owner of the object
Role.RELATED = "$related"; // any User with a relationship to the object Role.RELATED = '$related'; // any User with a relationship to the object
Role.AUTHENTICATED = "$authenticated"; // authenticated user Role.AUTHENTICATED = '$authenticated'; // authenticated user
Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user Role.UNAUTHENTICATED = '$unauthenticated'; // authenticated user
Role.EVERYONE = "$everyone"; // everyone Role.EVERYONE = '$everyone'; // everyone
/** /**
* Add custom handler for roles. * Add custom handler for roles.
@ -93,7 +93,7 @@ module.exports = function(Role) {
Role.registerResolver(Role.OWNER, function(role, context, callback) { Role.registerResolver(Role.OWNER, function(role, context, callback) {
if (!context || !context.model || !context.modelId) { if (!context || !context.model || !context.modelId) {
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, false); if (callback) callback(null, false);
}); });
return; return;
} }
@ -152,13 +152,13 @@ module.exports = function(Role) {
modelClass.findById(modelId, function(err, inst) { modelClass.findById(modelId, function(err, inst) {
if (err || !inst) { if (err || !inst) {
debug('Model not found for id %j', modelId); debug('Model not found for id %j', modelId);
callback && callback(err, false); if (callback) callback(err, false);
return; return;
} }
debug('Model found: %j', inst); debug('Model found: %j', inst);
var ownerId = inst.userId || inst.owner; var ownerId = inst.userId || inst.owner;
if (ownerId) { if (ownerId) {
callback && callback(null, matches(ownerId, userId)); if (callback) callback(null, matches(ownerId, userId));
return; return;
} else { } else {
// Try to follow belongsTo // Try to follow belongsTo
@ -166,19 +166,21 @@ module.exports = function(Role) {
var rel = modelClass.relations[r]; var rel = modelClass.relations[r];
if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) { if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) {
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel); debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
inst[r](function(err, user) { inst[r](processRelatedUser);
if (!err && user) {
debug('User found: %j', user.id);
callback && callback(null, matches(user.id, userId));
} else {
callback && callback(err, false);
}
});
return; return;
} }
} }
debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId); debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId);
callback && callback(null, false); if (callback) callback(null, false);
}
function processRelatedUser(err, user) {
if (!err && user) {
debug('User found: %j', user.id);
if (callback) callback(null, matches(user.id, userId));
} else {
if (callback) callback(err, false);
}
} }
}); });
}; };
@ -186,7 +188,7 @@ module.exports = function(Role) {
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) { Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
if (!context) { if (!context) {
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, false); if (callback) callback(null, false);
}); });
return; return;
} }
@ -202,19 +204,19 @@ module.exports = function(Role) {
*/ */
Role.isAuthenticated = function isAuthenticated(context, callback) { Role.isAuthenticated = function isAuthenticated(context, callback) {
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, context.isAuthenticated()); if (callback) callback(null, context.isAuthenticated());
}); });
}; };
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) { Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, !context || !context.isAuthenticated()); if (callback) callback(null, !context || !context.isAuthenticated());
}); });
}); });
Role.registerResolver(Role.EVERYONE, function(role, context, callback) { Role.registerResolver(Role.EVERYONE, function(role, context, callback) {
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, true); // Always true if (callback) callback(null, true); // Always true
}); });
}); });
@ -245,7 +247,7 @@ module.exports = function(Role) {
if (context.principals.length === 0) { if (context.principals.length === 0) {
debug('isInRole() returns: false'); debug('isInRole() returns: false');
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, false); if (callback) callback(null, false);
}); });
return; return;
} }
@ -262,7 +264,7 @@ module.exports = function(Role) {
if (inRole) { if (inRole) {
debug('isInRole() returns: %j', inRole); debug('isInRole() returns: %j', inRole);
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, true); if (callback) callback(null, true);
}); });
return; return;
} }
@ -270,11 +272,11 @@ module.exports = function(Role) {
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
this.findOne({where: {name: role}}, function(err, result) { this.findOne({where: {name: role}}, function(err, result) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
return; return;
} }
if (!result) { if (!result) {
callback && callback(null, false); if (callback) callback(null, false);
return; return;
} }
debug('Role found: %j', result); debug('Role found: %j', result);
@ -303,7 +305,7 @@ module.exports = function(Role) {
} }
}, function(inRole) { }, function(inRole) {
debug('isInRole() returns: %j', inRole); debug('isInRole() returns: %j', inRole);
callback && callback(null, inRole); if (callback) callback(null, inRole);
}); });
}); });
@ -315,8 +317,8 @@ module.exports = function(Role) {
* @param {Function} callback * @param {Function} callback
* *
* @callback {Function} callback * @callback {Function} callback
* @param err * @param {Error=} err
* @param {String[]} An array of role ids * @param {String[]} roles An array of role ids
*/ */
Role.getRoles = function(context, callback) { Role.getRoles = function(context, callback) {
if (!(context instanceof AccessContext)) { if (!(context instanceof AccessContext)) {
@ -354,8 +356,8 @@ module.exports = function(Role) {
// Check against the role mappings // Check against the role mappings
var principalType = p.type || undefined; var principalType = p.type || undefined;
var principalId = p.id == null ? undefined : p.id; var principalId = p.id == null ? undefined : p.id;
if(typeof principalId !== 'string' && principalId != null) { if (typeof principalId !== 'string' && principalId != null) {
principalId = principalId.toString(); principalId = principalId.toString();
} }
@ -371,13 +373,13 @@ module.exports = function(Role) {
principalId: principalId}}, function(err, mappings) { principalId: principalId}}, function(err, mappings) {
debug('Role mappings found: %s %j', err, mappings); debug('Role mappings found: %s %j', err, mappings);
if (err) { if (err) {
done && done(err); if (done) done(err);
return; return;
} }
mappings.forEach(function(m) { mappings.forEach(function(m) {
addRole(m.roleId); addRole(m.roleId);
}); });
done && done(); if (done) done();
}); });
}); });
} }
@ -385,7 +387,7 @@ module.exports = function(Role) {
async.parallel(inRoleTasks, function(err, results) { async.parallel(inRoleTasks, function(err, results) {
debug('getRoles() returns: %j %j', err, roles); debug('getRoles() returns: %j %j', err, roles);
callback && callback(err, roles); if (callback) callback(err, roles);
}); });
}; };
}; };

View File

@ -23,14 +23,14 @@ module.exports = function(Scope) {
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {AccessRequest} result The access permission * @param {AccessRequest} result The access permission
*/ */
Scope.checkPermission = function (scope, model, property, accessType, callback) { Scope.checkPermission = function(scope, model, property, accessType, callback) {
var ACL = loopback.ACL; var ACL = loopback.ACL;
assert(ACL, assert(ACL,
'ACL model must be defined before Scope.checkPermission is called'); 'ACL model must be defined before Scope.checkPermission is called');
this.findOne({where: {name: scope}}, function (err, scope) { this.findOne({where: {name: scope}}, function(err, scope) {
if (err) { if (err) {
callback && callback(err); if (callback) callback(err);
} else { } else {
var aclModel = loopback.getModelByType(ACL); var aclModel = loopback.getModelByType(ACL);
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);

File diff suppressed because it is too large Load Diff

View File

@ -4,24 +4,26 @@
"lib/application.js", "lib/application.js",
"lib/loopback.js", "lib/loopback.js",
"lib/registry.js", "lib/registry.js",
"lib/access-context.js",
{ "title": "Base models", "depth": 2 }, { "title": "Base models", "depth": 2 },
"lib/model.js", "lib/model.js",
"lib/persisted-model.js", "lib/persisted-model.js",
{ "title": "Middleware", "depth": 2 }, { "title": "Middleware", "depth": 2 },
"lib/middleware/rest.js", "lib/middleware/rest.js",
"lib/middleware/static.js",
"lib/middleware/status.js", "lib/middleware/status.js",
"lib/middleware/token.js", "lib/middleware/token.js",
"lib/middleware/urlNotFound.js", "lib/middleware/urlNotFound.js",
{ "title": "Built-in models", "depth": 2 }, { "title": "Built-in models", "depth": 2 },
"common/models/access-token.js", "common/models/access-token.js",
"common/models/acl.js", "common/models/acl.js",
"common/models/scope.js",
"common/models/application.js", "common/models/application.js",
"common/models/change.js",
"common/models/email.js", "common/models/email.js",
"common/models/role-mapping.js",
"common/models/role.js", "common/models/role.js",
"common/models/user.js", "common/models/role-mapping.js",
"common/models/change.js" "common/models/scope.js",
"common/models/user.js"
], ],
"assets": "/docs/assets" "assets": "/docs/assets"
} }

View File

@ -9,9 +9,7 @@ var schema = {
var Color = app.model('color', schema); var Color = app.model('color', schema);
app.dataSource('db', {adapter: 'memory'}); app.dataSource('db', {adapter: 'memory'}).attach(Color);
Color.dataSource('db');
Color.create({name: 'red'}); Color.create({name: 'red'});
Color.create({name: 'green'}); Color.create({name: 'green'});

29
example/context/app.js Normal file
View File

@ -0,0 +1,29 @@
var loopback = require('../../');
var app = loopback();
// Create a LoopBack context for all requests
app.use(loopback.context());
// Store a request property in the context
app.use(function saveHostToContext(req, res, next) {
var ns = loopback.getCurrentContext();
ns.set('host', req.host);
next();
});
app.use(loopback.rest());
var Color = loopback.createModel('color', { 'name': String });
Color.beforeRemote('**', function (ctx, unused, next) {
// Inside LoopBack code, you can read the property from the context
var ns = loopback.getCurrentContext();
console.log('Request to host', ns && ns.get('host'));
next();
});
app.dataSource('db', { connector: 'memory' });
app.model(Color, { dataSource: 'db' });
app.listen(3000, function() {
console.log('A list of colors is available at http://localhost:3000/colors');
});

View File

@ -20,4 +20,4 @@ Color.all(function () {
app.listen(3000); app.listen(3000);
console.log('a list of colors is available at http://localhost:300/colors'); console.log('a list of colors is available at http://localhost:3000/colors');

View File

@ -38,13 +38,13 @@ function AccessContext(context) {
this.method = context.method; this.method = context.method;
this.sharedMethod = context.sharedMethod; this.sharedMethod = context.sharedMethod;
this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass; this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass;
if(this.sharedMethod) { if (this.sharedMethod) {
this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]); this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
} else { } else {
this.methodNames = []; this.methodNames = [];
} }
if(this.sharedMethod) { if (this.sharedMethod) {
this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod); this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
} }
@ -100,7 +100,7 @@ AccessContext.permissionOrder = {
* @param {String} [principalName] The principal name * @param {String} [principalName] The principal name
* @returns {boolean} * @returns {boolean}
*/ */
AccessContext.prototype.addPrincipal = function (principalType, principalId, principalName) { AccessContext.prototype.addPrincipal = function(principalType, principalId, principalName) {
var principal = new Principal(principalType, principalId, principalName); var principal = new Principal(principalType, principalId, principalName);
for (var i = 0; i < this.principals.length; i++) { for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i]; var p = this.principals[i];
@ -126,7 +126,6 @@ AccessContext.prototype.getUserId = function() {
return null; return null;
}; };
/** /**
* Get the application id * Get the application id
* @returns {*} * @returns {*}
@ -149,14 +148,14 @@ AccessContext.prototype.isAuthenticated = function() {
return !!(this.getUserId() || this.getAppId()); return !!(this.getUserId() || this.getAppId());
}; };
/** /*!
* Print debug info for access context. * Print debug info for access context.
*/ */
AccessContext.prototype.debug = function() { AccessContext.prototype.debug = function() {
if(debug.enabled) { if (debug.enabled) {
debug('---AccessContext---'); debug('---AccessContext---');
if(this.principals && this.principals.length) { if (this.principals && this.principals.length) {
debug('principals:'); debug('principals:');
this.principals.forEach(function(principal) { this.principals.forEach(function(principal) {
debug('principal: %j', principal); debug('principal: %j', principal);
@ -169,7 +168,7 @@ AccessContext.prototype.debug = function() {
debug('property %s', this.property); debug('property %s', this.property);
debug('method %s', this.method); debug('method %s', this.method);
debug('accessType %s', this.accessType); debug('accessType %s', this.accessType);
if(this.accessToken) { if (this.accessToken) {
debug('accessToken:'); debug('accessToken:');
debug(' id %j', this.accessToken.id); debug(' id %j', this.accessToken.id);
debug(' ttl %j', this.accessToken.ttl); debug(' ttl %j', this.accessToken.ttl);
@ -206,9 +205,9 @@ Principal.SCOPE = 'SCOPE';
/** /**
* Compare if two principals are equal * Compare if two principals are equal
* Returns true if argument principal is equal to this principal. * Returns true if argument principal is equal to this principal.
* @param {Object} principal The other principal * @param {Object} p The other principal
*/ */
Principal.prototype.equals = function (p) { Principal.prototype.equals = function(p) {
if (p instanceof Principal) { if (p instanceof Principal) {
return this.type === p.type && String(this.id) === String(p.id); return this.type === p.type && String(this.id) === String(p.id);
} }
@ -250,7 +249,7 @@ function AccessRequest(model, property, accessType, permission, methodNames) {
* *
* @returns {Boolean} * @returns {Boolean}
*/ */
AccessRequest.prototype.isWildcard = function () { AccessRequest.prototype.isWildcard = function() {
return this.model === AccessContext.ALL || return this.model === AccessContext.ALL ||
this.property === AccessContext.ALL || this.property === AccessContext.ALL ||
this.accessType === AccessContext.ALL; this.accessType === AccessContext.ALL;
@ -268,7 +267,7 @@ AccessRequest.prototype.exactlyMatches = function(acl) {
var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1; var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
var matchesAccessType = acl.accessType === this.accessType; var matchesAccessType = acl.accessType === this.accessType;
if(matchesModel && matchesAccessType) { if (matchesModel && matchesAccessType) {
return matchesProperty || matchesMethodName; return matchesProperty || matchesMethodName;
} }
@ -286,7 +285,7 @@ AccessRequest.prototype.isAllowed = function() {
}; };
AccessRequest.prototype.debug = function() { AccessRequest.prototype.debug = function() {
if(debug.enabled) { if (debug.enabled) {
debug('---AccessRequest---'); debug('---AccessRequest---');
debug(' model %s', this.model); debug(' model %s', this.model);
debug(' property %s', this.property); debug(' property %s', this.property);
@ -300,6 +299,3 @@ AccessRequest.prototype.debug = function() {
module.exports.AccessContext = AccessContext; module.exports.AccessContext = AccessContext;
module.exports.Principal = Principal; module.exports.Principal = Principal;
module.exports.AccessRequest = AccessRequest; module.exports.AccessRequest = AccessRequest;

View File

@ -2,15 +2,15 @@
* Module dependencies. * Module dependencies.
*/ */
var DataSource = require('loopback-datasource-juggler').DataSource var DataSource = require('loopback-datasource-juggler').DataSource;
, registry = require('./registry') var registry = require('./registry');
, assert = require('assert') var assert = require('assert');
, fs = require('fs') var fs = require('fs');
, extend = require('util')._extend var extend = require('util')._extend;
, _ = require('underscore') var _ = require('underscore');
, RemoteObjects = require('strong-remoting') var RemoteObjects = require('strong-remoting');
, stringUtils = require('underscore.string') var stringUtils = require('underscore.string');
, path = require('path'); var path = require('path');
/** /**
* The `App` object represents a Loopback application. * The `App` object represents a Loopback application.
@ -41,7 +41,7 @@ function App() {
* Export the app prototype. * Export the app prototype.
*/ */
var app = exports = module.exports = {}; var app = module.exports = {};
/** /**
* Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). * Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
@ -50,13 +50,13 @@ var app = exports = module.exports = {};
* @returns {RemoteObjects} * @returns {RemoteObjects}
*/ */
app.remotes = function () { app.remotes = function() {
if(this._remotes) { if (this._remotes) {
return this._remotes; return this._remotes;
} else { } else {
var options = {}; var options = {};
if(this.get) { if (this.get) {
options = this.get('remoting'); options = this.get('remoting');
} }
@ -68,10 +68,10 @@ app.remotes = function () {
* Remove a route by reference. * Remove a route by reference.
*/ */
app.disuse = function (route) { app.disuse = function(route) {
if(this.stack) { if (this.stack) {
for (var i = 0; i < this.stack.length; i++) { for (var i = 0; i < this.stack.length; i++) {
if(this.stack[i].route === route) { if (this.stack[i].route === route) {
this.stack.splice(i, 1); this.stack.splice(i, 1);
} }
} }
@ -102,7 +102,7 @@ app.disuse = function (route) {
* @returns {ModelConstructor} the model class * @returns {ModelConstructor} the model class
*/ */
app.model = function (Model, config) { app.model = function(Model, config) {
var isPublic = true; var isPublic = true;
if (arguments.length > 1) { if (arguments.length > 1) {
config = config || {}; config = config || {};
@ -166,7 +166,7 @@ app.model = function (Model, config) {
* ```js * ```js
* var models = app.models(); * var models = app.models();
* *
* models.forEach(function (Model) { * models.forEach(function(Model) {
* console.log(Model.modelName); // color * console.log(Model.modelName); // color
* }); * });
* ``` * ```
@ -205,7 +205,7 @@ app.model = function (Model, config) {
* @returns {Array} Array of model classes. * @returns {Array} Array of model classes.
*/ */
app.models = function () { app.models = function() {
return this._models || (this._models = []); return this._models || (this._models = []);
}; };
@ -215,7 +215,7 @@ app.models = function () {
* @param {String} name The data source name * @param {String} name The data source name
* @param {Object} config The data source config * @param {Object} config The data source config
*/ */
app.dataSource = function (name, config) { app.dataSource = function(name, config) {
var ds = dataSourcesFromConfig(config, this.connectors); var ds = dataSourcesFromConfig(config, this.connectors);
this.dataSources[name] = this.dataSources[name] =
this.dataSources[classify(name)] = this.dataSources[classify(name)] =
@ -248,7 +248,7 @@ app.connector = function(name, connector) {
* @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). * @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
*/ */
app.remoteObjects = function () { app.remoteObjects = function() {
var result = {}; var result = {};
this.remotes().classes().forEach(function(sharedClass) { this.remotes().classes().forEach(function(sharedClass) {
@ -263,9 +263,9 @@ app.remoteObjects = function () {
* @triggers `mounted` events on shared class constructors (models) * @triggers `mounted` events on shared class constructors (models)
*/ */
app.handler = function (type, options) { app.handler = function(type, options) {
var handlers = this._handlers || (this._handlers = {}); var handlers = this._handlers || (this._handlers = {});
if(handlers[type]) { if (handlers[type]) {
return handlers[type]; return handlers[type];
} }
@ -301,21 +301,21 @@ app.enableAuth = function() {
var modelSettings = Model.settings || {}; var modelSettings = Model.settings || {};
var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401; var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401;
if(!req.accessToken){ if (!req.accessToken) {
errStatusCode = 401; errStatusCode = 401;
} }
if(Model.checkAccess) { if (Model.checkAccess) {
Model.checkAccess( Model.checkAccess(
req.accessToken, req.accessToken,
modelId, modelId,
method, method,
ctx, ctx,
function(err, allowed) { function(err, allowed) {
if(err) { if (err) {
console.log(err); console.log(err);
next(err); next(err);
} else if(allowed) { } else if (allowed) {
next(); next();
} else { } else {
@ -358,7 +358,7 @@ function dataSourcesFromConfig(config, connectorRegistry) {
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') {
var name = config.connector; var name = config.connector;
if (connectorRegistry[name]) { if (connectorRegistry[name]) {
config.connector = connectorRegistry[name]; config.connector = connectorRegistry[name];
@ -380,14 +380,16 @@ function configureModel(ModelCtor, config, app) {
var dataSource = config.dataSource; var dataSource = config.dataSource;
if(dataSource) { if (dataSource) {
if(typeof dataSource === 'string') { if (typeof dataSource === 'string') {
dataSource = app.dataSources[dataSource]; dataSource = app.dataSources[dataSource];
} }
assert(dataSource instanceof DataSource, assert(
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' + dataSource instanceof DataSource,
config.dataSource +'"'); ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
config.dataSource + '"'
);
} }
config = extend({}, config); config = extend({}, config);

View File

@ -8,11 +8,11 @@ module.exports = Connector;
* Module dependencies. * Module dependencies.
*/ */
var EventEmitter = require('events').EventEmitter var EventEmitter = require('events').EventEmitter;
, debug = require('debug')('connector') var debug = require('debug')('connector');
, util = require('util') var util = require('util');
, inherits = util.inherits var inherits = util.inherits;
, assert = require('assert'); var assert = require('assert');
/** /**
* Create a new `Connector` with the given `options`. * Create a new `Connector` with the given `options`.
@ -38,9 +38,9 @@ inherits(Connector, EventEmitter);
* Create an connector instance from a JugglingDB adapter. * Create an connector instance from a JugglingDB adapter.
*/ */
Connector._createJDBAdapter = function (jdbModule) { Connector._createJDBAdapter = function(jdbModule) {
var fauxSchema = {}; var fauxSchema = {};
jdbModule.initialize(fauxSchema, function () { jdbModule.initialize(fauxSchema, function() {
// connected // connected
}); });
}; };
@ -49,6 +49,6 @@ Connector._createJDBAdapter = function (jdbModule) {
* Add default crud operations from a JugglingDB adapter. * Add default crud operations from a JugglingDB adapter.
*/ */
Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) { Connector.prototype._addCrudOperationsFromJDBAdapter = function(connector) {
}; };

View File

@ -2,10 +2,10 @@
* Dependencies. * Dependencies.
*/ */
var mailer = require('nodemailer') var mailer = require('nodemailer');
, assert = require('assert') var assert = require('assert');
, debug = require('debug')('loopback:connector:mail') var debug = require('debug')('loopback:connector:mail');
, loopback = require('../loopback'); var loopback = require('../loopback');
/** /**
* Export the MailConnector class. * Export the MailConnector class.
@ -24,19 +24,19 @@ function MailConnector(settings) {
var transports = settings.transports; var transports = settings.transports;
//if transports is not in settings object AND settings.transport exists //if transports is not in settings object AND settings.transport exists
if(!transports && settings.transport){ if (!transports && settings.transport) {
//then wrap single transport in an array and assign to transports //then wrap single transport in an array and assign to transports
transports = [settings.transport]; transports = [settings.transport];
} }
if(!transports){ if (!transports) {
transports = []; transports = [];
} }
this.transportsIndex = {}; this.transportsIndex = {};
this.transports = []; this.transports = [];
if(loopback.isServer) { if (loopback.isServer) {
transports.forEach(this.setupTransport.bind(this)); transports.forEach(this.setupTransport.bind(this));
} }
} }
@ -48,7 +48,6 @@ MailConnector.initialize = function(dataSource, callback) {
MailConnector.prototype.DataAccessObject = Mailer; MailConnector.prototype.DataAccessObject = Mailer;
/** /**
* Add a transport to the available transports. See https://github.com/andris9/Nodemailer#setting-up-a-transport-method. * Add a transport to the available transports. See https://github.com/andris9/Nodemailer#setting-up-a-transport-method.
* *
@ -132,7 +131,7 @@ MailConnector.prototype.defaultTransport = function() {
* @param {Function} callback Called after the e-mail is sent or the sending failed * @param {Function} callback Called after the e-mail is sent or the sending failed
*/ */
Mailer.send = function (options, fn) { Mailer.send = function(options, fn) {
var dataSource = this.dataSource; var dataSource = this.dataSource;
var settings = dataSource && dataSource.settings; var settings = dataSource && dataSource.settings;
var connector = dataSource.connector; var connector = dataSource.connector;
@ -140,13 +139,13 @@ Mailer.send = function (options, fn) {
var transport = connector.transportForName(options.transport); var transport = connector.transportForName(options.transport);
if(!transport) { if (!transport) {
transport = connector.defaultTransport(); transport = connector.defaultTransport();
} }
if(debug.enabled || settings && settings.debug) { if (debug.enabled || settings && settings.debug) {
console.log('Sending Mail:'); console.log('Sending Mail:');
if(options.transport) { if (options.transport) {
console.log('\t TRANSPORT:', options.transport); console.log('\t TRANSPORT:', options.transport);
} }
console.log('\t TO:', options.to); console.log('\t TO:', options.to);
@ -156,12 +155,12 @@ Mailer.send = function (options, fn) {
console.log('\t HTML:', options.html); console.log('\t HTML:', options.html);
} }
if(transport) { if (transport) {
assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport'); assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport');
transport.sendMail(options, fn); transport.sendMail(options, fn);
} else { } else {
console.warn('Warning: No email transport specified for sending email.' console.warn('Warning: No email transport specified for sending email.' +
+ ' Setup a transport to send mail messages.'); ' Setup a transport to send mail messages.');
process.nextTick(function() { process.nextTick(function() {
fn(null, options); fn(null, options);
}); });
@ -172,7 +171,7 @@ Mailer.send = function (options, fn) {
* Send an email instance using `modelInstance.send()`. * Send an email instance using `modelInstance.send()`.
*/ */
Mailer.prototype.send = function (fn) { Mailer.prototype.send = function(fn) {
this.constructor.send(this, fn); this.constructor.send(this, fn);
}; };

View File

@ -8,12 +8,12 @@ module.exports = Memory;
* Module dependencies. * Module dependencies.
*/ */
var Connector = require('./base-connector') var Connector = require('./base-connector');
, debug = require('debug')('memory') var debug = require('debug')('memory');
, util = require('util') var util = require('util');
, inherits = util.inherits var inherits = util.inherits;
, assert = require('assert') var assert = require('assert');
, JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory'); var JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory');
/** /**
* Create a new `Memory` connector with the given `options`. * Create a new `Memory` connector with the given `options`.

View File

@ -1,4 +1,3 @@
var express = require('express');
var path = require('path'); var path = require('path');
var middlewares = exports; var middlewares = exports;
@ -12,7 +11,7 @@ function safeRequire(m) {
} }
function createMiddlewareNotInstalled(memberName, moduleName) { function createMiddlewareNotInstalled(memberName, moduleName) {
return function () { return function() {
var msg = 'The middleware loopback.' + memberName + ' is not installed.\n' + var msg = 'The middleware loopback.' + memberName + ' is not installed.\n' +
'Run `npm install --save ' + moduleName + '` to fix the problem.'; 'Run `npm install --save ' + moduleName + '` to fix the problem.';
throw new Error(msg); throw new Error(msg);
@ -47,7 +46,7 @@ for (var m in middlewareModules) {
// serve-favicon requires a path // serve-favicon requires a path
var favicon = middlewares.favicon; var favicon = middlewares.favicon;
middlewares.favicon = function (icon, options) { middlewares.favicon = function(icon, options) {
icon = icon || path.join(__dirname, '../favicon.ico'); icon = icon || path.join(__dirname, '../favicon.ico');
return favicon(icon, options); return favicon(icon, options);
}; };

View File

@ -2,13 +2,14 @@
* Module dependencies. * Module dependencies.
*/ */
var express = require('express') var express = require('express');
, proto = require('./application') var loopbackExpress = require('./server-app');
, fs = require('fs') var proto = require('./application');
, ejs = require('ejs') var fs = require('fs');
, path = require('path') var ejs = require('ejs');
, merge = require('util')._extend var path = require('path');
, assert = require('assert'); var merge = require('util')._extend;
var assert = require('assert');
/** /**
* LoopBack core module. It provides static properties and * LoopBack core module. It provides static properties and
@ -24,11 +25,13 @@ var express = require('express')
* @property {String} mime * @property {String} mime
* @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property. * @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property.
* @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property. * @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property.
* @property {String} faviconFile Path to a default favicon shipped with LoopBack.
* Use as follows: `app.use(require('serve-favicon')(loopback.faviconFile));`
* @class loopback * @class loopback
* @header loopback * @header loopback
*/ */
var loopback = exports = module.exports = createApplication; var loopback = module.exports = createApplication;
/*! /*!
* Framework version. * Framework version.
@ -50,7 +53,7 @@ loopback.mime = express.mime;
*/ */
function createApplication() { function createApplication() {
var app = express(); var app = loopbackExpress();
merge(app, proto); merge(app, proto);
@ -117,13 +120,35 @@ if (loopback.isServer) {
if (loopback.isServer) { if (loopback.isServer) {
fs fs
.readdirSync(path.join(__dirname, 'middleware')) .readdirSync(path.join(__dirname, '..', 'server', 'middleware'))
.filter(function (file) { .filter(function(file) {
return file.match(/\.js$/); return file.match(/\.js$/);
}) })
.forEach(function (m) { .forEach(function(m) {
loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m); loopback[m.replace(/\.js$/, '')] = require('../server/middleware/' + m);
}); });
loopback.urlNotFound = loopback['url-not-found'];
delete loopback['url-not-found'];
}
/*
* Expose path to the default favicon file
*
* ***only in node***
*/
if (loopback.isServer) {
/*!
* Path to a default favicon shipped with LoopBack.
*
* **Example**
*
* ```js
* app.use(require('serve-favicon')(loopback.faviconFile));
* ```
*/
loopback.faviconFile = path.resolve(__dirname, '../favicon.ico');
} }
/*! /*!
@ -138,10 +163,10 @@ loopback.errorHandler.title = 'Loopback';
* @param {Object} options (optional) * @param {Object} options (optional)
*/ */
loopback.remoteMethod = function (fn, options) { loopback.remoteMethod = function(fn, options) {
fn.shared = true; fn.shared = true;
if(typeof options === 'object') { if (typeof options === 'object') {
Object.keys(options).forEach(function (key) { Object.keys(options).forEach(function(key) {
fn[key] = options[key]; fn[key] = options[key];
}); });
} }
@ -158,12 +183,16 @@ loopback.remoteMethod = function (fn, options) {
* @returns {Function} * @returns {Function}
*/ */
loopback.template = function (file) { loopback.template = function(file) {
var templates = this._templates || (this._templates = {}); var templates = this._templates || (this._templates = {});
var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8')); var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
return ejs.compile(str); return ejs.compile(str);
}; };
loopback.getCurrentContext = function() {
// A placeholder method, see lib/middleware/context.js for the real version
return null;
};
/*! /*!
* Built in models / services * Built in models / services

View File

@ -57,7 +57,7 @@ var stringUtils = require('underscore.string');
* *
* ```js * ```js
* MyModel.on('deletedAll', function(where) { * MyModel.on('deletedAll', function(where) {
* if(where) { * if (where) {
* console.log('all models where ', where, ' have been deleted'); * console.log('all models where ', where, ' have been deleted');
* // => all models where * // => all models where
* // => {price: {gt: 100}} * // => {price: {gt: 100}}
@ -98,7 +98,7 @@ var Model = module.exports = registry.modelBuilder.define('Model');
* Called when a model is extended. * Called when a model is extended.
*/ */
Model.setup = function () { Model.setup = function() {
var ModelCtor = this; var ModelCtor = this;
var options = this.settings; var options = this.settings;
var typeName = this.modelName; var typeName = this.modelName;
@ -119,17 +119,17 @@ Model.setup = function () {
}); });
// support remoting prototype methods // support remoting prototype methods
ModelCtor.sharedCtor = function (data, id, fn) { ModelCtor.sharedCtor = function(data, id, fn) {
var ModelCtor = this; var ModelCtor = this;
if(typeof data === 'function') { if (typeof data === 'function') {
fn = data; fn = data;
data = null; data = null;
id = null; id = null;
} else if (typeof id === 'function') { } else if (typeof id === 'function') {
fn = id; fn = id;
if(typeof data !== 'object') { if (typeof data !== 'object') {
id = data; id = data;
data = null; data = null;
} else { } else {
@ -137,17 +137,17 @@ Model.setup = function () {
} }
} }
if(id && data) { if (id && data) {
var model = new ModelCtor(data); var model = new ModelCtor(data);
model.id = id; model.id = id;
fn(null, model); fn(null, model);
} else if(data) { } else if (data) {
fn(null, new ModelCtor(data)); fn(null, new ModelCtor(data));
} else if(id) { } else if (id) {
ModelCtor.findById(id, function (err, model) { ModelCtor.findById(id, function(err, model) {
if(err) { if (err) {
fn(err); fn(err);
} else if(model) { } else if (model) {
fn(null, model); fn(null, model);
} else { } else {
err = new Error('could not find a model with id ' + id); err = new Error('could not find a model with id ' + id);
@ -175,34 +175,34 @@ Model.setup = function () {
ModelCtor.sharedCtor.returns = {root: true}; ModelCtor.sharedCtor.returns = {root: true};
// before remote hook // before remote hook
ModelCtor.beforeRemote = function (name, fn) { ModelCtor.beforeRemote = function(name, fn) {
var self = this; var self = this;
if(this.app) { if (this.app) {
var remotes = this.app.remotes(); var remotes = this.app.remotes();
var className = self.modelName; var className = self.modelName;
remotes.before(className + '.' + name, function (ctx, next) { remotes.before(className + '.' + name, function(ctx, next) {
fn(ctx, ctx.result, next); fn(ctx, ctx.result, next);
}); });
} else { } else {
var args = arguments; var args = arguments;
this.once('attached', function () { this.once('attached', function() {
self.beforeRemote.apply(self, args); self.beforeRemote.apply(self, args);
}); });
} }
}; };
// after remote hook // after remote hook
ModelCtor.afterRemote = function (name, fn) { ModelCtor.afterRemote = function(name, fn) {
var self = this; var self = this;
if(this.app) { if (this.app) {
var remotes = this.app.remotes(); var remotes = this.app.remotes();
var className = self.modelName; var className = self.modelName;
remotes.after(className + '.' + name, function (ctx, next) { remotes.after(className + '.' + name, function(ctx, next) {
fn(ctx, ctx.result, next); fn(ctx, ctx.result, next);
}); });
} else { } else {
var args = arguments; var args = arguments;
this.once('attached', function () { this.once('attached', function() {
self.afterRemote.apply(self, args); self.afterRemote.apply(self, args);
}); });
} }
@ -246,11 +246,11 @@ Model.setup = function () {
*/ */
var _aclModel = null; var _aclModel = null;
Model._ACL = function getACL(ACL) { Model._ACL = function getACL(ACL) {
if(ACL !== undefined) { if (ACL !== undefined) {
// The function is used as a setter // The function is used as a setter
_aclModel = ACL; _aclModel = ACL;
} }
if(_aclModel) { if (_aclModel) {
return _aclModel; return _aclModel;
} }
var aclModel = registry.getModel('ACL'); var aclModel = registry.getModel('ACL');
@ -276,7 +276,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) {
var aclModel = Model._ACL(); var aclModel = Model._ACL();
ctx = ctx || {}; ctx = ctx || {};
if(typeof ctx === 'function' && callback === undefined) { if (typeof ctx === 'function' && callback === undefined) {
callback = ctx; callback = ctx;
ctx = {}; ctx = {};
} }
@ -291,7 +291,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) {
accessType: this._getAccessTypeForMethod(sharedMethod), accessType: this._getAccessTypeForMethod(sharedMethod),
remotingContext: ctx remotingContext: ctx
}, function(err, accessRequest) { }, function(err, accessRequest) {
if(err) return callback(err); if (err) return callback(err);
callback(null, accessRequest.isAllowed()); callback(null, accessRequest.isAllowed());
}); });
}; };
@ -304,7 +304,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) {
*/ */
Model._getAccessTypeForMethod = function(method) { Model._getAccessTypeForMethod = function(method) {
if(typeof method === 'string') { if (typeof method === 'string') {
method = {name: method}; method = {name: method};
} }
assert( assert(
@ -314,7 +314,7 @@ Model._getAccessTypeForMethod = function(method) {
var ACL = Model._ACL(); var ACL = Model._ACL();
switch(method.name) { switch (method.name) {
case'create': case'create':
return ACL.WRITE; return ACL.WRITE;
case 'updateOrCreate': case 'updateOrCreate':
@ -353,7 +353,7 @@ Model._getAccessTypeForMethod = function(method) {
Model.getApp = function(callback) { Model.getApp = function(callback) {
var Model = this; var Model = this;
if(this.app) { if (this.app) {
callback(null, this.app); callback(null, this.app);
} else { } else {
Model.once('attached', function() { Model.once('attached', function() {
@ -378,7 +378,7 @@ Model.getApp = function(callback) {
*/ */
Model.remoteMethod = function(name, options) { Model.remoteMethod = function(name, options) {
if(options.isStatic === undefined) { if (options.isStatic === undefined) {
options.isStatic = true; options.isStatic = true;
} }
this.sharedClass.defineMethod(name, options); this.sharedClass.defineMethod(name, options);
@ -423,7 +423,7 @@ Model.hasOneRemoting = function(relationName, relation, define) {
}, fn); }, fn);
}; };
Model.hasManyRemoting = function (relationName, relation, define) { Model.hasManyRemoting = function(relationName, relation, define) {
var pathName = (relation.options.http && relation.options.http.path) || relationName; var pathName = (relation.options.http && relation.options.http.path) || relationName;
var toModelName = relation.modelTo.modelName; var toModelName = relation.modelTo.modelName;
@ -457,7 +457,7 @@ Model.hasManyRemoting = function (relationName, relation, define) {
description: 'Foreign key for ' + relationName, required: true, description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}}, http: {source: 'path'}},
description: 'Delete a related item by id for ' + relationName, description: 'Delete a related item by id for ' + relationName,
returns: {} returns: []
}, destroyByIdFunc); }, destroyByIdFunc);
var updateByIdFunc = this.prototype['__updateById__' + relationName]; var updateByIdFunc = this.prototype['__updateById__' + relationName];
@ -502,7 +502,7 @@ Model.hasManyRemoting = function (relationName, relation, define) {
description: 'Foreign key for ' + relationName, required: true, description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}}, http: {source: 'path'}},
description: 'Remove the ' + relationName + ' relation to an item by id', description: 'Remove the ' + relationName + ' relation to an item by id',
returns: {} returns: []
}, removeFunc); }, removeFunc);
// FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD? // FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD?
@ -519,7 +519,7 @@ Model.hasManyRemoting = function (relationName, relation, define) {
rest: { rest: {
// After hook to map exists to 200/404 for HEAD // After hook to map exists to 200/404 for HEAD
after: function(ctx, cb) { after: function(ctx, cb) {
if(ctx.result === false) { if (ctx.result === false) {
var modelName = ctx.method.sharedClass.name; var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id'); var id = ctx.getArgByName('id');
var msg = 'Unknown "' + modelName + '" id "' + id + '".'; var msg = 'Unknown "' + modelName + '" id "' + id + '".';
@ -536,11 +536,21 @@ Model.hasManyRemoting = function (relationName, relation, define) {
}; };
Model.scopeRemoting = function(scopeName, scope, define) { Model.scopeRemoting = function(scopeName, scope, define) {
var pathName = (scope.options && scope.options.http && scope.options.http.path) var pathName =
|| scopeName; (scope.options && scope.options.http && scope.options.http.path) || scopeName;
var isStatic = scope.isStatic; var isStatic = scope.isStatic;
var toModelName = scope.modelTo.modelName; var toModelName = scope.modelTo.modelName;
// https://github.com/strongloop/loopback/issues/811
// Check if the scope is for a hasMany relation
var relation = this.relations[scopeName];
if (relation && relation.modelTo) {
// For a relation with through model, the toModelName should be the one
// from the target model
toModelName = relation.modelTo.modelName;
}
define('__get__' + scopeName, { define('__get__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName}, http: {verb: 'get', path: '/' + pathName},
@ -592,10 +602,12 @@ Model.nestRemoting = function(relationName, options, cb) {
var paramName = options.paramName || 'nk'; var paramName = options.paramName || 'nk';
var http = [].concat(sharedToClass.http || [])[0]; var http = [].concat(sharedToClass.http || [])[0];
var httpPath;
var acceptArgs;
if (relation.multiple) { if (relation.multiple) {
var httpPath = pathName + '/:' + paramName; httpPath = pathName + '/:' + paramName;
var acceptArgs = [ acceptArgs = [
{ {
arg: paramName, type: 'any', http: { source: 'path' }, arg: paramName, type: 'any', http: { source: 'path' },
description: 'Foreign key for ' + relation.name, description: 'Foreign key for ' + relation.name,
@ -603,8 +615,8 @@ Model.nestRemoting = function(relationName, options, cb) {
} }
]; ];
} else { } else {
var httpPath = pathName; httpPath = pathName;
var acceptArgs = []; acceptArgs = [];
} }
// A method should return the method name to use, if it is to be // A method should return the method name to use, if it is to be
@ -721,4 +733,3 @@ Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
// setup the initial model // setup the initial model
Model.setup(); Model.setup();

View File

@ -37,7 +37,7 @@ PersistedModel.setup = function setupPersistedModel() {
var PersistedModel = this; var PersistedModel = this;
// enable change tracking (usually for replication) // enable change tracking (usually for replication)
if(this.settings.trackChanges) { if (this.settings.trackChanges) {
PersistedModel._defineChangeModel(); PersistedModel._defineChangeModel();
PersistedModel.once('dataSourceAttached', function() { PersistedModel.once('dataSourceAttached', function() {
PersistedModel.enableChangeTracking(); PersistedModel.enableChangeTracking();
@ -53,9 +53,9 @@ PersistedModel.setup = function setupPersistedModel() {
function throwNotAttached(modelName, methodName) { function throwNotAttached(modelName, methodName) {
throw new Error( throw new Error(
'Cannot call ' + modelName + '.'+ methodName + '().' 'Cannot call ' + modelName + '.' + methodName + '().' +
+ ' The ' + methodName + ' method has not been setup.' ' The ' + methodName + ' method has not been setup.' +
+ ' The PersistedModel has not been correctly attached to a DataSource!' ' The PersistedModel has not been correctly attached to a DataSource!'
); );
} }
@ -79,12 +79,12 @@ function convertNullToNotFoundError(ctx, cb) {
/** /**
* Create new instance of Model class, saved in database * Create new instance of Model class, saved in database
* *
* @param {Object} data Optional data object. * @param {Object}|[{Object}] data Optional data object. Can be either a single model instance or an array of instances.
* @param {Function} cb Callback function with `cb(err, obj)` signature, * @param {Function} cb Callback function with `cb(err, obj)` signature,
* where `err` is error object and `obj` is null or Model instance. * where `err` is error object and `obj` is null or Model instance.
*/ */
PersistedModel.create = function (data, callback) { PersistedModel.create = function(data, callback) {
throwNotAttached(this.modelName, 'create'); throwNotAttached(this.modelName, 'create');
}; };
@ -226,7 +226,7 @@ PersistedModel.deleteAll = PersistedModel.destroyAll;
* Example: * Example:
* *
*```js *```js
* Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) { * Employee.updateAll({managerId: 'x001'}, {managerId: 'x002'}, function(err, count) {
* ... * ...
* }); * });
* ``` * ```
@ -277,7 +277,7 @@ PersistedModel.deleteById = PersistedModel.destroyById;
* @param {Function} cb Callback function called with (err, count). * @param {Function} cb Callback function called with (err, count).
*/ */
PersistedModel.count = function (where, cb) { PersistedModel.count = function(where, cb) {
throwNotAttached(this.modelName, 'count'); throwNotAttached(this.modelName, 'count');
}; };
@ -290,7 +290,7 @@ PersistedModel.count = function (where, cb) {
* @param {Function} [callback] Callback function called with (err, obj). * @param {Function} [callback] Callback function called with (err, obj).
*/ */
PersistedModel.prototype.save = function (options, callback) { PersistedModel.prototype.save = function(options, callback) {
var Model = this.constructor; var Model = this.constructor;
if (typeof options == 'function') { if (typeof options == 'function') {
@ -298,7 +298,7 @@ PersistedModel.prototype.save = function (options, callback) {
options = {}; options = {};
} }
callback = callback || function () { callback = callback || function() {
}; };
options = options || {}; options = options || {};
@ -322,7 +322,7 @@ PersistedModel.prototype.save = function (options, callback) {
return save(); return save();
} }
inst.isValid(function (valid) { inst.isValid(function(valid) {
if (valid) { if (valid) {
save(); save();
} else { } else {
@ -337,12 +337,12 @@ PersistedModel.prototype.save = function (options, callback) {
// then save // then save
function save() { function save() {
inst.trigger('save', function (saveDone) { inst.trigger('save', function(saveDone) {
inst.trigger('update', function (updateDone) { inst.trigger('update', function(updateDone) {
Model.upsert(inst, function(err) { Model.upsert(inst, function(err) {
inst._initProperties(data); inst._initProperties(data);
updateDone.call(inst, function () { updateDone.call(inst, function() {
saveDone.call(inst, function () { saveDone.call(inst, function() {
callback(err, inst); callback(err, inst);
}); });
}); });
@ -357,7 +357,7 @@ PersistedModel.prototype.save = function (options, callback) {
* @returns {Boolean} Returns true if the data model is new; false otherwise. * @returns {Boolean} Returns true if the data model is new; false otherwise.
*/ */
PersistedModel.prototype.isNewRecord = function () { PersistedModel.prototype.isNewRecord = function() {
throwNotAttached(this.constructor.modelName, 'isNewRecord'); throwNotAttached(this.constructor.modelName, 'isNewRecord');
}; };
@ -367,7 +367,7 @@ PersistedModel.prototype.isNewRecord = function () {
* @param {Function} callback Callback function. * @param {Function} callback Callback function.
*/ */
PersistedModel.prototype.destroy = function (cb) { PersistedModel.prototype.destroy = function(cb) {
throwNotAttached(this.constructor.modelName, 'destroy'); throwNotAttached(this.constructor.modelName, 'destroy');
}; };
@ -440,7 +440,7 @@ PersistedModel.prototype.setId = function(val) {
PersistedModel.prototype.getId = function() { PersistedModel.prototype.getId = function() {
var data = this.toObject(); var data = this.toObject();
if(!data) return; if (!data) return;
return data[this.getIdName()]; return data[this.getIdName()];
}; };
@ -464,7 +464,7 @@ PersistedModel.getIdName = function() {
var Model = this; var Model = this;
var ds = Model.getDataSource(); var ds = Model.getDataSource();
if(ds.idName) { if (ds.idName) {
return ds.idName(Model.modelName); return ds.idName(Model.modelName);
} else { } else {
return 'id'; return 'id';
@ -513,7 +513,7 @@ PersistedModel.setupRemoting = function() {
// For GET, return {exists: true|false} as is // For GET, return {exists: true|false} as is
return cb(); return cb();
} }
if(!ctx.result.exists) { if (!ctx.result.exists) {
var modelName = ctx.method.sharedClass.name; var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id'); var id = ctx.getArgByName('id');
var msg = 'Unknown "' + modelName + '" id "' + id + '".'; var msg = 'Unknown "' + modelName + '" id "' + id + '".';
@ -594,7 +594,7 @@ PersistedModel.setupRemoting = function() {
http: {verb: 'put', path: '/'} http: {verb: 'put', path: '/'}
}); });
if(options.trackChanges) { if (options.trackChanges) {
setRemoting(PersistedModel, 'diff', { setRemoting(PersistedModel, 'diff', {
description: 'Get a set of deltas and conflicts since the given checkpoint', description: 'Get a set of deltas and conflicts since the given checkpoint',
accepts: [ accepts: [
@ -607,8 +607,8 @@ PersistedModel.setupRemoting = function() {
}); });
setRemoting(PersistedModel, 'changes', { setRemoting(PersistedModel, 'changes', {
description: 'Get the changes to a model since a given checkpoint.' description: 'Get the changes to a model since a given checkpoint.' +
+ 'Provide a filter object to reduce the number of results returned.', 'Provide a filter object to reduce the number of results returned.',
accepts: [ accepts: [
{arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'}, {arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'},
{arg: 'filter', type: 'object', description: 'Only include changes that match this filter'} {arg: 'filter', type: 'object', description: 'Only include changes that match this filter'}
@ -683,12 +683,12 @@ PersistedModel.diff = function(since, remoteChanges, callback) {
*/ */
PersistedModel.changes = function(since, filter, callback) { PersistedModel.changes = function(since, filter, callback) {
if(typeof since === 'function') { if (typeof since === 'function') {
filter = {}; filter = {};
callback = since; callback = since;
since = -1; since = -1;
} }
if(typeof filter === 'function') { if (typeof filter === 'function') {
callback = filter; callback = filter;
since = -1; since = -1;
filter = {}; filter = {};
@ -708,18 +708,18 @@ PersistedModel.changes = function(since, filter, callback) {
checkpoint: {gt: since}, checkpoint: {gt: since},
modelName: this.modelName modelName: this.modelName
}, function(err, changes) { }, function(err, changes) {
if(err) return callback(err); if (err) return callback(err);
var ids = changes.map(function(change) { var ids = changes.map(function(change) {
return change.getModelId(); return change.getModelId();
}); });
filter.where[idName] = {inq: ids}; filter.where[idName] = {inq: ids};
model.find(filter, function(err, models) { model.find(filter, function(err, models) {
if(err) return callback(err); if (err) return callback(err);
var modelIds = models.map(function(m) { var modelIds = models.map(function(m) {
return m[idName].toString(); return m[idName].toString();
}); });
callback(null, changes.filter(function(ch) { callback(null, changes.filter(function(ch) {
if(ch.type() === Change.DELETE) return true; if (ch.type() === Change.DELETE) return true;
return modelIds.indexOf(ch.modelId) > -1; return modelIds.indexOf(ch.modelId) > -1;
})); }));
}); });
@ -735,7 +735,7 @@ PersistedModel.changes = function(since, filter, callback) {
PersistedModel.checkpoint = function(cb) { PersistedModel.checkpoint = function(cb) {
var Checkpoint = this.getChangeModel().getCheckpointModel(); var Checkpoint = this.getChangeModel().getCheckpointModel();
this.getSourceId(function(err, sourceId) { this.getSourceId(function(err, sourceId) {
if(err) return cb(err); if (err) return cb(err);
Checkpoint.create({ Checkpoint.create({
sourceId: sourceId sourceId: sourceId
}, cb); }, cb);
@ -772,11 +772,11 @@ PersistedModel.currentCheckpoint = function(cb) {
PersistedModel.replicate = function(since, targetModel, options, callback) { PersistedModel.replicate = function(since, targetModel, options, callback) {
var lastArg = arguments[arguments.length - 1]; var lastArg = arguments[arguments.length - 1];
if(typeof lastArg === 'function' && arguments.length > 1) { if (typeof lastArg === 'function' && arguments.length > 1) {
callback = lastArg; callback = lastArg;
} }
if(typeof since === 'function' && since.modelName) { if (typeof since === 'function' && since.modelName) {
targetModel = since; targetModel = since;
since = -1; since = -1;
} }
@ -796,7 +796,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
); );
callback = callback || function defaultReplicationCallback(err) { callback = callback || function defaultReplicationCallback(err) {
if(err) throw err; if (err) throw err;
}; };
var tasks = [ var tasks = [
@ -820,7 +820,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
function createSourceUpdates(_diff, cb) { function createSourceUpdates(_diff, cb) {
diff = _diff; diff = _diff;
diff.conflicts = diff.conflicts || []; diff.conflicts = diff.conflicts || [];
if(diff && diff.deltas && diff.deltas.length) { if (diff && diff.deltas && diff.deltas.length) {
sourceModel.createUpdates(diff.deltas, cb); sourceModel.createUpdates(diff.deltas, cb);
} else { } else {
// nothing to replicate // nothing to replicate
@ -838,7 +838,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
} }
function done(err) { function done(err) {
if(err) return callback(err); if (err) return callback(err);
var conflicts = diff.conflicts.map(function(change) { var conflicts = diff.conflicts.map(function(change) {
return new Change.Conflict( return new Change.Conflict(
@ -846,11 +846,11 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
); );
}); });
if(conflicts.length) { if (conflicts.length) {
sourceModel.emit('conflicts', conflicts); sourceModel.emit('conflicts', conflicts);
} }
callback && callback(null, conflicts); if (callback) callback(null, conflicts);
} }
}; };
@ -869,21 +869,21 @@ PersistedModel.createUpdates = function(deltas, cb) {
var tasks = []; var tasks = [];
deltas.forEach(function(change) { deltas.forEach(function(change) {
var change = new Change(change); change = new Change(change);
var type = change.type(); var type = change.type();
var update = {type: type, change: change}; var update = {type: type, change: change};
switch(type) { switch (type) {
case Change.CREATE: case Change.CREATE:
case Change.UPDATE: case Change.UPDATE:
tasks.push(function(cb) { tasks.push(function(cb) {
Model.findById(change.modelId, function(err, inst) { Model.findById(change.modelId, function(err, inst) {
if(err) return cb(err); if (err) return cb(err);
if(!inst) { if (!inst) {
console.error('missing data for change:', change); console.error('missing data for change:', change);
return cb && cb(new Error('missing data for change: ' return cb &&
+ change.modelId)); cb(new Error('missing data for change: ' + change.modelId));
} }
if(inst.toObject) { if (inst.toObject) {
update.data = inst.toObject(); update.data = inst.toObject();
} else { } else {
update.data = inst; update.data = inst;
@ -892,15 +892,15 @@ PersistedModel.createUpdates = function(deltas, cb) {
cb(); cb();
}); });
}); });
break; break;
case Change.DELETE: case Change.DELETE:
updates.push(update); updates.push(update);
break; break;
} }
}); });
async.parallel(tasks, function(err) { async.parallel(tasks, function(err) {
if(err) return cb(err); if (err) return cb(err);
cb(null, updates); cb(null, updates);
}); });
}; };
@ -921,7 +921,7 @@ PersistedModel.bulkUpdate = function(updates, callback) {
var Change = this.getChangeModel(); var Change = this.getChangeModel();
updates.forEach(function(update) { updates.forEach(function(update) {
switch(update.type) { switch (update.type) {
case Change.UPDATE: case Change.UPDATE:
case Change.CREATE: case Change.CREATE:
// var model = new Model(update.data); // var model = new Model(update.data);
@ -930,13 +930,13 @@ PersistedModel.bulkUpdate = function(updates, callback) {
var model = new Model(update.data); var model = new Model(update.data);
model.save(cb); model.save(cb);
}); });
break; break;
case Change.DELETE: case Change.DELETE:
var data = {}; var data = {};
data[idName] = update.change.modelId; data[idName] = update.change.modelId;
var model = new Model(data); var model = new Model(data);
tasks.push(model.destroy.bind(model)); tasks.push(model.destroy.bind(model));
break; break;
} }
}); });
@ -969,7 +969,7 @@ PersistedModel.getChangeModel = function() {
PersistedModel.getSourceId = function(cb) { PersistedModel.getSourceId = function(cb) {
var dataSource = this.dataSource; var dataSource = this.dataSource;
if(!dataSource) { if (!dataSource) {
this.once('dataSourceAttached', this.getSourceId.bind(this, cb)); this.once('dataSourceAttached', this.getSourceId.bind(this, cb));
} }
assert( assert(
@ -1005,21 +1005,21 @@ PersistedModel.enableChangeTracking = function() {
Model.on('deletedAll', cleanup); Model.on('deletedAll', cleanup);
if(runtime.isServer) { if (runtime.isServer) {
// initial cleanup // initial cleanup
cleanup(); cleanup();
// cleanup // cleanup
setInterval(cleanup, cleanupInterval); setInterval(cleanup, cleanupInterval);
}
function cleanup() { function cleanup() {
Model.rectifyAllChanges(function(err) { Model.rectifyAllChanges(function(err) {
if(err) { if (err) {
console.error(Model.modelName + ' Change Cleanup Error:'); console.error(Model.modelName + ' Change Cleanup Error:');
console.error(err); console.error(err);
} }
}); });
}
} }
}; };
@ -1028,12 +1028,14 @@ PersistedModel._defineChangeModel = function() {
assert(BaseChangeModel, assert(BaseChangeModel,
'Change model must be defined before enabling change replication'); 'Change model must be defined before enabling change replication');
return this.Change = BaseChangeModel.extend(this.modelName + '-change', this.Change = BaseChangeModel.extend(this.modelName + '-change',
{}, {},
{ {
trackModel: this trackModel: this
} }
); );
return this.Change;
}; };
PersistedModel.rectifyAllChanges = function(callback) { PersistedModel.rectifyAllChanges = function(callback) {
@ -1048,7 +1050,7 @@ PersistedModel.rectifyAllChanges = function(callback) {
*/ */
PersistedModel.handleChangeError = function(err) { PersistedModel.handleChangeError = function(err) {
if(err) { if (err) {
console.error(Model.modelName + ' Change Tracking Error:'); console.error(Model.modelName + ' Change Tracking Error:');
console.error(err); console.error(err);
} }

View File

@ -46,7 +46,7 @@ registry.modelBuilder = new ModelBuilder();
* 'Author', * 'Author',
* { * {
* firstName: 'string', * firstName: 'string',
* lastName: 'string * lastName: 'string'
* }, * },
* { * {
* relations: { * relations: {
@ -66,7 +66,7 @@ registry.modelBuilder = new ModelBuilder();
* name: 'Author', * name: 'Author',
* properties: { * properties: {
* firstName: 'string', * firstName: 'string',
* lastName: 'string * lastName: 'string'
* }, * },
* relations: { * relations: {
* books: { * books: {
@ -84,7 +84,7 @@ registry.modelBuilder = new ModelBuilder();
* @header loopback.createModel * @header loopback.createModel
*/ */
registry.createModel = function (name, properties, options) { registry.createModel = function(name, properties, options) {
if (arguments.length === 1 && typeof name === 'object') { if (arguments.length === 1 && typeof name === 'object') {
var config = name; var config = name;
name = config.name; name = config.name;
@ -98,7 +98,7 @@ registry.createModel = function (name, properties, options) {
options = options || {}; options = options || {};
var BaseModel = options.base || options.super; var BaseModel = options.base || options.super;
if(typeof BaseModel === 'string') { if (typeof BaseModel === 'string') {
var baseName = BaseModel; var baseName = BaseModel;
BaseModel = this.getModel(BaseModel); BaseModel = this.getModel(BaseModel);
@ -121,7 +121,7 @@ registry.createModel = function (name, properties, options) {
// try to attach // try to attach
try { try {
this.autoAttachModel(model); this.autoAttachModel(model);
} catch(e) {} } catch (e) {}
return model; return model;
}; };
@ -145,6 +145,25 @@ function buildModelOptionsFromConfig(config) {
return options; return options;
} }
/*
* Add the acl entry to the acls
* @param {Object[]} acls
* @param {Object} acl
*/
function addACL(acls, acl) {
for (var i = 0, n = acls.length; i < n; i++) {
// Check if there is a matching acl to be overriden
if (acls[i].property === acl.property &&
acls[i].accessType === acl.accessType &&
acls[i].principalType === acl.principalType &&
acls[i].principalId === acl.principalId) {
acls[i] = acl;
return;
}
}
acls.push(acl);
}
/** /**
* Alter an existing Model class. * Alter an existing Model class.
* @param {Model} ModelCtor The model constructor to alter. * @param {Model} ModelCtor The model constructor to alter.
@ -157,12 +176,51 @@ function buildModelOptionsFromConfig(config) {
registry.configureModel = function(ModelCtor, config) { registry.configureModel = function(ModelCtor, config) {
var settings = ModelCtor.settings; var settings = ModelCtor.settings;
var modelName = ModelCtor.modelName;
if (config.relations) { // Relations
if (typeof config.relations === 'object' && config.relations !== null) {
var relations = settings.relations = settings.relations || {}; var relations = settings.relations = settings.relations || {};
Object.keys(config.relations).forEach(function(key) { Object.keys(config.relations).forEach(function(key) {
// FIXME: [rfeng] We probably should check if the relation exists
relations[key] = extend(relations[key] || {}, config.relations[key]); relations[key] = extend(relations[key] || {}, config.relations[key]);
}); });
} else if (config.relations != null) {
console.warn('The relations property of `%s` configuration ' +
'must be an object', modelName);
}
// ACLs
if (Array.isArray(config.acls)) {
var acls = settings.acls = settings.acls || [];
config.acls.forEach(function(acl) {
addACL(acls, acl);
});
} else if (config.acls != null) {
console.warn('The acls property of `%s` configuration ' +
'must be an array of objects', modelName);
}
// Settings
var excludedProperties = {
base: true,
'super': true,
relations: true,
acls: true,
dataSource: true
};
if (typeof config.options === 'object' && config.options !== null) {
for (var p in config.options) {
if (!(p in excludedProperties)) {
settings[p] = config.options[p];
} else {
console.warn('Property `%s` cannot be reconfigured for `%s`',
p, modelName);
}
}
} else if (config.options != null) {
console.warn('The options property of `%s` configuration ' +
'must be an object', modelName);
} }
// It's important to attach the datasource after we have updated // It's important to attach the datasource after we have updated
@ -173,17 +231,17 @@ registry.configureModel = function(ModelCtor, config) {
': config.dataSource must be an instance of DataSource'); ': config.dataSource must be an instance of DataSource');
ModelCtor.attachTo(config.dataSource); ModelCtor.attachTo(config.dataSource);
debug('Attached model `%s` to dataSource `%s`', debug('Attached model `%s` to dataSource `%s`',
ModelCtor.definition.name, config.dataSource.name); modelName, config.dataSource.name);
} else if (config.dataSource === null) { } else if (config.dataSource === null) {
debug('Model `%s` is not attached to any DataSource by configuration.', debug('Model `%s` is not attached to any DataSource by configuration.',
ModelCtor.definition.name); modelName);
} else { } else {
debug('Model `%s` is not attached to any DataSource, possibly by a mistake.', debug('Model `%s` is not attached to any DataSource, possibly by a mistake.',
ModelCtor.definition.name); modelName);
console.warn( console.warn(
'The configuration of `%s` is missing `dataSource` property.\n' + 'The configuration of `%s` is missing `dataSource` property.\n' +
'Use `null` or `false` to mark models not attached to any data source.', 'Use `null` or `false` to mark models not attached to any data source.',
ModelCtor.definition.name); modelName);
} }
}; };
@ -228,8 +286,8 @@ registry.getModelByType = function(modelType) {
assert(typeof modelType === 'function', assert(typeof modelType === 'function',
'The model type must be a constructor'); 'The model type must be a constructor');
var models = this.modelBuilder.models; var models = this.modelBuilder.models;
for(var m in models) { for (var m in models) {
if(models[m].prototype instanceof modelType) { if (models[m].prototype instanceof modelType) {
return models[m]; return models[m];
} }
} }
@ -248,10 +306,10 @@ registry.getModelByType = function(modelType) {
* @header loopback.createDataSource(name, options) * @header loopback.createDataSource(name, options)
*/ */
registry.createDataSource = function (name, options) { registry.createDataSource = function(name, options) {
var self = this; var self = this;
var ds = new DataSource(name, options, self.modelBuilder); var ds = new DataSource(name, options, self.modelBuilder);
ds.createModel = function (name, properties, settings) { ds.createModel = function(name, properties, settings) {
settings = settings || {}; settings = settings || {};
var BaseModel = settings.base || settings.super; var BaseModel = settings.base || settings.super;
if (!BaseModel) { if (!BaseModel) {
@ -270,7 +328,7 @@ registry.createDataSource = function (name, options) {
return ModelCtor; return ModelCtor;
}; };
if(ds.settings && ds.settings.defaultForType) { if (ds.settings && ds.settings.defaultForType) {
this.setDefaultDataSourceForType(ds.settings.defaultForType, ds); this.setDefaultDataSourceForType(ds.settings.defaultForType, ds);
} }
@ -286,13 +344,13 @@ registry.createDataSource = function (name, options) {
* @header loopback.memory([name]) * @header loopback.memory([name])
*/ */
registry.memory = function (name) { registry.memory = function(name) {
name = name || 'default'; name = name || 'default';
var memory = ( var memory = (
this._memoryDataSources || (this._memoryDataSources = {}) this._memoryDataSources || (this._memoryDataSources = {})
)[name]; )[name];
if(!memory) { if (!memory) {
memory = this._memoryDataSources[name] = this.createDataSource({ memory = this._memoryDataSources[name] = this.createDataSource({
connector: 'memory' connector: 'memory'
}); });
@ -313,7 +371,7 @@ registry.memory = function (name) {
registry.setDefaultDataSourceForType = function(type, dataSource) { registry.setDefaultDataSourceForType = function(type, dataSource) {
var defaultDataSources = this.defaultDataSources; var defaultDataSources = this.defaultDataSources;
if(!(dataSource instanceof DataSource)) { if (!(dataSource instanceof DataSource)) {
dataSource = this.createDataSource(dataSource); dataSource = this.createDataSource(dataSource);
} }
@ -346,19 +404,21 @@ registry.autoAttach = function() {
var ModelCtor = models[modelName]; var ModelCtor = models[modelName];
// Only auto attach if the model doesn't have an explicit data source // Only auto attach if the model doesn't have an explicit data source
if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) { if (ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) {
this.autoAttachModel(ModelCtor); this.autoAttachModel(ModelCtor);
} }
}, this); }, this);
}; };
registry.autoAttachModel = function(ModelCtor) { registry.autoAttachModel = function(ModelCtor) {
if(ModelCtor.autoAttach) { if (ModelCtor.autoAttach) {
var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach); var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach);
assert(ds instanceof DataSource, 'cannot autoAttach model "' assert(
+ ModelCtor.modelName ds instanceof DataSource,
+ '". No dataSource found of type ' + ModelCtor.autoAttach); 'cannot autoAttach model "' + ModelCtor.modelName +
'". No dataSource found of type ' + ModelCtor.autoAttach
);
ModelCtor.attachTo(ds); ModelCtor.attachTo(ds);
} }

View File

@ -19,4 +19,3 @@ runtime.isBrowser = typeof window !== 'undefined';
*/ */
runtime.isServer = !runtime.isBrowser; runtime.isServer = !runtime.isBrowser;

266
lib/server-app.js Normal file
View File

@ -0,0 +1,266 @@
var assert = require('assert');
var express = require('express');
var merge = require('util')._extend;
var PhaseList = require('loopback-phase').PhaseList;
var debug = require('debug')('loopback:app');
var pathToRegexp = require('path-to-regexp');
var proto = {};
module.exports = function loopbackExpress() {
var app = express();
app.__expressLazyRouter = app.lazyrouter;
merge(app, proto);
return app;
};
/**
* Register a middleware using a factory function and a JSON config.
*
* **Example**
*
* ```js
* app.middlewareFromConfig(compression, {
* enabled: true,
* phase: 'initial',
* params: {
* threshold: 128
* }
* });
* ```
*
* @param {function} factory The factory function creating a middleware handler.
* Typically a result of `require()` call, e.g. `require('compression')`.
* @options {Object} config The configuration.
* @property {String} phase The phase to register the middleware in.
* @property {Boolean} [enabled] Whether the middleware is enabled.
* Default: `true`.
* @property {Array|*} [params] The arguments to pass to the factory
* function. Either an array of arguments,
* or the value of the first argument when the factory expects
* a single argument only.
* @property {Array|string|RegExp} [paths] Optional list of paths limiting
* the scope of the middleware.
*
* @returns {object} this (fluent API)
*
* @header app.middlewareFromConfig(factory, config)
*/
proto.middlewareFromConfig = function(factory, config) {
assert(typeof factory === 'function', '"factory" must be a function');
assert(typeof config === 'object', '"config" must be an object');
assert(typeof config.phase === 'string' && config.phase,
'"config.phase" must be a non-empty string');
if (config.enabled === false)
return;
var params = config.params;
if (params === undefined) {
params = [];
} else if (!Array.isArray(params)) {
params = [params];
}
var handler = factory.apply(null, params);
this.middleware(config.phase, config.paths || [], handler);
return this;
};
/**
* Register (new) middleware phases.
*
* If all names are new, then the phases are added just before "routes" phase.
* Otherwise the provided list of names is merged with the existing phases
* in such way that the order of phases is preserved.
*
* **Examples**
*
* ```js
* // built-in phases:
* // initial, session, auth, parse, routes, files, final
*
* app.defineMiddlewarePhases('custom');
* // new list of phases
* // initial, session, auth, parse, custom, routes, files, final
*
* app.defineMiddlewarePhases([
* 'initial', 'postinit', 'preauth', 'routes', 'subapps'
* ]);
* // new list of phases
* // initial, postinit, preauth, session, auth, parse, custom,
* // routes, subapps, files, final
* ```
*
* @param {string|Array.<string>} nameOrArray A phase name or a list of phase
* names to add.
*
* @returns {object} this (fluent API)
*
* @header app.defineMiddlewarePhases(nameOrArray)
*/
proto.defineMiddlewarePhases = function(nameOrArray) {
this.lazyrouter();
if (Array.isArray(nameOrArray)) {
this._requestHandlingPhases.zipMerge(nameOrArray);
} else {
this._requestHandlingPhases.addBefore('routes', nameOrArray);
}
return this;
};
/**
* Register a middleware handler to be executed in a given phase.
* @param {string} name The phase name, e.g. "init" or "routes".
* @param {Array|string|RegExp} [paths] Optional list of paths limiting
* the scope of the middleware.
* String paths are interpreted as expressjs path patterns,
* regular expressions are used as-is.
* @param {function} handler The middleware handler, one of
* `function(req, res, next)` or
* `function(err, req, res, next)`
* @returns {object} this (fluent API)
*
* @header app.middleware(name, handler)
*/
proto.middleware = function(name, paths, handler) {
this.lazyrouter();
if (handler === undefined && typeof paths === 'function') {
handler = paths;
paths = [];
}
if (typeof paths === 'string' || paths instanceof RegExp) {
paths = [paths];
}
assert(typeof name === 'string' && name, '"name" must be a non-empty string');
assert(typeof handler === 'function', '"handler" must be a function');
assert(Array.isArray(paths), '"paths" must be an array');
var fullName = name;
var handlerName = handler.name || '(anonymous)';
var hook = 'use';
var m = name.match(/^(.+):(before|after)$/);
if (m) {
name = m[1];
hook = m[2];
}
var phase = this._requestHandlingPhases.find(name);
if (!phase)
throw new Error('Unknown middleware phase ' + name);
var matches = createRequestMatcher(paths);
var wrapper;
if (handler.length === 4) {
// handler is function(err, req, res, next)
debug('Add error handler %j to phase %j', handlerName, fullName);
wrapper = function errorHandler(ctx, next) {
if (ctx.err && matches(ctx.req)) {
var err = ctx.err;
ctx.err = undefined;
handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
} else {
next();
}
};
} else {
// handler is function(req, res, next)
debug('Add middleware %j to phase %j', handlerName , fullName);
wrapper = function regularHandler(ctx, next) {
if (ctx.err || !matches(ctx.req)) {
next();
} else {
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
}
};
}
phase[hook](wrapper);
return this;
};
function createRequestMatcher(paths) {
if (!paths.length) {
return function requestMatcher(req) { return true; };
}
var checks = paths.map(function(p) {
return pathToRegexp(p, {
sensitive: true,
strict: false,
end: false
});
});
return function requestMatcher(req) {
return checks.some(function(regex) {
return regex.test(req.url);
});
};
}
function storeErrorAndContinue(ctx, next) {
return function(err) {
if (err) ctx.err = err;
next();
};
}
// Install our custom PhaseList-based handler into the app
proto.lazyrouter = function() {
var self = this;
if (self._router) return;
self.__expressLazyRouter();
// Storing the fn in another property of the router object
// allows us to call the method with the router as `this`
// without the need to use slow `call` or `apply`.
self._router.__expressHandle = self._router.handle;
self._requestHandlingPhases = new PhaseList();
self._requestHandlingPhases.add([
'initial', 'session', 'auth', 'parse',
'routes', 'files', 'final'
]);
// In order to pass error into express router, we have
// to pass it to a middleware executed from within the router.
// This is achieved by adding a phase-handler that wraps the error
// into `req` object and then a router-handler that unwraps the error
// and calls `next(err)`.
// It is important to register these two handlers at the very beginning,
// before any other handlers are added.
self.middleware('routes', function wrapError(err, req, res, next) {
req.__err = err;
next();
});
self.use(function unwrapError(req, res, next) {
var err = req.__err;
req.__err = undefined;
next(err);
});
self.middleware('routes', function runRootHandlers(req, res, next) {
self._router.__expressHandle(req, res, next);
});
// Overwrite the original handle() function provided by express,
// replace it with our implementation based on PhaseList
self._router.handle = function(req, res, next) {
var ctx = { req: req, res: res };
self._requestHandlingPhases.run(ctx, function(err) {
next(err || ctx.err);
});
};
};

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback", "name": "loopback",
"version": "2.7.0", "version": "2.8.0",
"description": "LoopBack: Open Source Framework for Node.js", "description": "LoopBack: Open Source Framework for Node.js",
"homepage": "http://loopback.io", "homepage": "http://loopback.io",
"keywords": [ "keywords": [
@ -36,13 +36,17 @@
"bcryptjs": "~2.0.2", "bcryptjs": "~2.0.2",
"body-parser": "~1.8.1", "body-parser": "~1.8.1",
"canonical-json": "0.0.4", "canonical-json": "0.0.4",
"continuation-local-storage": "~3.1.1",
"debug": "~2.0.0", "debug": "~2.0.0",
"ejs": "~1.0.0", "ejs": "~1.0.0",
"express": "4.x", "express": "^4.10.2",
"inflection": "~1.4.2", "inflection": "~1.4.2",
"loopback-connector-remote": "^1.0.1", "loopback-connector-remote": "^1.0.1",
"loopback-phase": "^1.0.1",
"nodemailer": "~1.3.0", "nodemailer": "~1.3.0",
"nodemailer-stub-transport": "~0.1.4", "nodemailer-stub-transport": "~0.1.4",
"path-to-regexp": "^1.0.1",
"serve-favicon": "^2.1.6",
"strong-remoting": "^2.4.0", "strong-remoting": "^2.4.0",
"uid2": "0.0.3", "uid2": "0.0.3",
"underscore": "~1.7.0", "underscore": "~1.7.0",
@ -53,18 +57,20 @@
}, },
"devDependencies": { "devDependencies": {
"browserify": "~4.2.3", "browserify": "~4.2.3",
"chai": "~1.9.1", "chai": "^1.10.0",
"cookie-parser": "~1.3.3", "cookie-parser": "~1.3.3",
"errorhandler": "~1.2.0", "errorhandler": "~1.2.0",
"es5-shim": "^4.0.3", "es5-shim": "^4.0.3",
"grunt": "~0.4.5", "grunt": "^0.4.5",
"grunt-browserify": "~3.0.1", "grunt-browserify": "~3.0.1",
"grunt-cli": "^0.1.13", "grunt-cli": "^0.1.13",
"grunt-contrib-jshint": "~0.10.0", "grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-uglify": "~0.5.1", "grunt-contrib-uglify": "~0.5.1",
"grunt-contrib-watch": "~0.6.1", "grunt-contrib-watch": "~0.6.1",
"grunt-jscs": "^0.8.1",
"grunt-karma": "~0.9.0", "grunt-karma": "~0.9.0",
"grunt-mocha-test": "^0.11.0", "grunt-mocha-test": "^0.11.0",
"karma": "~0.12.23",
"karma-browserify": "~0.2.1", "karma-browserify": "~0.2.1",
"karma-chrome-launcher": "~0.1.4", "karma-chrome-launcher": "~0.1.4",
"karma-firefox-launcher": "~0.1.3", "karma-firefox-launcher": "~0.1.3",
@ -79,8 +85,7 @@
"mocha": "~1.21.4", "mocha": "~1.21.4",
"serve-favicon": "~2.1.3", "serve-favicon": "~2.1.3",
"strong-task-emitter": "0.0.x", "strong-task-emitter": "0.0.x",
"supertest": "~0.13.0", "supertest": "~0.13.0"
"karma": "~0.12.23"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -88,6 +93,7 @@
}, },
"browser": { "browser": {
"express": "./lib/browser-express.js", "express": "./lib/browser-express.js",
"./lib/server-app.js": "./lib/browser-express.js",
"connect": false, "connect": false,
"nodemailer": false "nodemailer": false
}, },

View File

@ -0,0 +1,118 @@
var loopback = require('../../lib/loopback');
var juggler = require('loopback-datasource-juggler');
var remoting = require('strong-remoting');
var cls = require('continuation-local-storage');
module.exports = context;
var name = 'loopback';
function createContext(scope) {
// Make the namespace globally visible via the process.context property
process.context = process.context || {};
var ns = process.context[scope];
if (!ns) {
ns = cls.createNamespace(scope);
process.context[scope] = ns;
// Set up loopback.getCurrentContext()
loopback.getCurrentContext = function() {
return ns && ns.active ? ns : null;
};
chain(juggler);
chain(remoting);
}
return ns;
}
/**
* Context middleware.
* ```js
* var app = loopback();
* app.use(loopback.context(options);
* app.use(loopback.rest());
* app.listen();
* ```
* @options {Object} [options] Options for context
* @property {String} name Context scope name.
* @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false.
* @header loopback.context([options])
*/
function context(options) {
options = options || {};
var scope = options.name || name;
var enableHttpContext = options.enableHttpContext || false;
var ns = createContext(scope);
// Return the middleware
return function contextHandler(req, res, next) {
if (req.loopbackContext) {
return next();
}
req.loopbackContext = ns;
// Bind req/res event emitters to the given namespace
ns.bindEmitter(req);
ns.bindEmitter(res);
// Create namespace for the request context
ns.run(function processRequestInContext(context) {
// Run the code in the context of the namespace
if (enableHttpContext) {
ns.set('http', {req: req, res: res}); // Set up the transport context
}
next();
});
};
}
/**
* Create a chained context
* @param {Object} child The child context
* @param {Object} parent The parent context
* @private
* @constructor
*/
function ChainedContext(child, parent) {
this.child = child;
this.parent = parent;
}
/*!
* Get the value by name from the context. If it doesn't exist in the child
* context, try the parent one
* @param {String} name Name of the context property
* @returns {*} Value of the context property
*/
ChainedContext.prototype.get = function(name) {
var val = this.child && this.child.get(name);
if (val === undefined) {
return this.parent && this.parent.get(name);
}
};
ChainedContext.prototype.set = function(name, val) {
if (this.child) {
return this.child.set(name, val);
} else {
return this.parent && this.parent.set(name, val);
}
};
ChainedContext.prototype.reset = function(name, val) {
if (this.child) {
return this.child.reset(name, val);
} else {
return this.parent && this.parent.reset(name, val);
}
};
function chain(child) {
if (typeof child.getCurrentContext === 'function') {
var childContext = new ChainedContext(child.getCurrentContext(),
loopback.getCurrentContext());
child.getCurrentContext = function() {
return childContext;
};
} else {
child.getCurrentContext = loopback.getCurrentContext;
}
}

View File

@ -0,0 +1 @@
module.exports = require('../../lib/express-middleware').favicon;

View File

@ -2,7 +2,8 @@
* Module dependencies. * Module dependencies.
*/ */
var loopback = require('../loopback'); var loopback = require('../../lib/loopback');
var async = require('async');
/*! /*!
* Export the middleware. * Export the middleware.
@ -12,7 +13,7 @@ module.exports = rest;
/** /**
* Expose models over REST. * Expose models over REST.
* *
* For example: * For example:
* ```js * ```js
* app.use(loopback.rest()); * app.use(loopback.rest());
@ -22,17 +23,30 @@ module.exports = rest;
*/ */
function rest() { function rest() {
var tokenParser = null; return function restApiHandler(req, res, next) {
return function (req, res, next) {
var app = req.app; var app = req.app;
var handler = app.handler('rest'); var restHandler = app.handler('rest');
if(req.url === '/routes') { if (req.url === '/routes') {
res.send(handler.adapter.allRoutes()); return res.send(restHandler.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) {
var preHandlers;
if (!preHandlers) {
preHandlers = [];
var remotingOptions = app.get('remoting') || {};
var contextOptions = remotingOptions.context;
if (contextOptions !== false) {
if (typeof contextOptions !== 'object')
contextOptions = {};
preHandlers.push(loopback.context(contextOptions));
}
if (app.isAuthEnabled) {
// NOTE(bajtos) It would be better to search app.models for a model // NOTE(bajtos) It would be better to search app.models for a model
// of type AccessToken instead of searching all loopback models. // of type AccessToken instead of searching all loopback models.
// Unfortunately that's not supported now. // Unfortunately that's not supported now.
@ -40,19 +54,12 @@ function rest() {
// https://github.com/strongloop/loopback/pull/167 // https://github.com/strongloop/loopback/pull/167
// https://github.com/strongloop/loopback/commit/f07446a // https://github.com/strongloop/loopback/commit/f07446a
var AccessToken = loopback.getModelByType(loopback.AccessToken); var AccessToken = loopback.getModelByType(loopback.AccessToken);
tokenParser = loopback.token({ model: AccessToken }); preHandlers.push(loopback.token({ model: AccessToken }));
} }
tokenParser(req, res, function(err) {
if (err) {
next(err);
} else {
handler(req, res, next);
}
});
} else {
handler(req, res, next);
} }
async.eachSeries(preHandlers.concat(restHandler), function(handler, done) {
handler(req, res, done);
}, next);
}; };
} }

View File

@ -0,0 +1,11 @@
/**
* Serve static assets of a LoopBack application.
*
* @param {string} root The root directory from which the static assets are to
* be served.
* @param {object} options Refer to
* [express documentation](http://expressjs.com/4x/api.html#express.static)
* for the full list of available options.
* @header loopback.static(root, [options])
*/
module.exports = require('express').static;

View File

@ -27,4 +27,3 @@ function status() {
}); });
}; };
} }

View File

@ -2,7 +2,7 @@
* Module dependencies. * Module dependencies.
*/ */
var loopback = require('../loopback'); var loopback = require('../../lib/loopback');
var assert = require('assert'); var assert = require('assert');
/*! /*!
@ -48,12 +48,13 @@ function token(options) {
var TokenModel = options.model || loopback.AccessToken; var TokenModel = options.model || loopback.AccessToken;
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(); if (req.accessToken !== undefined) return next();
TokenModel.findForRequest(req, options, function(err, token) { TokenModel.findForRequest(req, options, function(err, token) {
req.accessToken = token || null; req.accessToken = token || null;
var ctx = loopback.getCurrentContext();
if (ctx) ctx.set('accessToken', token);
next(err); next(err);
}); });
}; };
} }

View File

@ -1,4 +1,5 @@
var loopback = require('../'); var loopback = require('../');
var extend = require('util')._extend;
var Token = loopback.AccessToken.extend('MyToken'); var Token = loopback.AccessToken.extend('MyToken');
var ACL = loopback.ACL; var ACL = loopback.ACL;
@ -106,6 +107,38 @@ describe('AccessToken', function () {
done(); done();
}); });
}); });
describe('.findForRequest()', function() {
beforeEach(createTestingToken);
it('supports two-arg variant with no options', function(done) {
var expectedTokenId = this.token.id;
var req = mockRequest({
headers: { 'authorization': expectedTokenId }
});
Token.findForRequest(req, function(err, token) {
if (err) return done(err);
expect(token.id).to.eql(expectedTokenId);
done();
});
});
function mockRequest(opts) {
return extend(
{
method: 'GET',
url: '/a-test-path',
headers: {},
_params: {},
// express helpers
param: function(name) { return this._params[name]; },
header: function(name) { return this.headers[name]; }
},
opts);
}
});
}); });
describe('app.enableAuth()', function() { describe('app.enableAuth()', function() {
@ -143,6 +176,36 @@ describe('app.enableAuth()', function() {
.end(done); .end(done);
}); });
it('stores token in the context', function(done) {
var TestModel = loopback.createModel('TestModel', { base: 'Model' });
TestModel.getToken = function(cb) {
cb(null, loopback.getCurrentContext().get('accessToken') || null);
};
TestModel.remoteMethod('getToken', {
returns: { arg: 'token', type: 'object' },
http: { verb: 'GET', path: '/token' }
});
var app = loopback();
app.model(TestModel, { dataSource: null });
app.enableAuth();
app.use(loopback.context());
app.use(loopback.token({ model: Token }));
app.use(loopback.rest());
var token = this.token;
request(app)
.get('/TestModels/token?_format=json')
.set('authorization', token.id)
.expect(200)
.expect('Content-Type', /json/)
.end(function(err, res) {
if (err) return done(err);
expect(res.body.token.id).to.eql(token.id);
done();
});
});
}); });
function createTestingToken(done) { function createTestingToken(done) {

View File

@ -1,5 +1,7 @@
var async = require('async');
var path = require('path'); var path = require('path');
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
var http = require('http');
var loopback = require('../'); var loopback = require('../');
var PersistedModel = loopback.PersistedModel; var PersistedModel = loopback.PersistedModel;
@ -7,6 +9,300 @@ var describe = require('./util/describe');
var it = require('./util/it'); var it = require('./util/it');
describe('app', function() { describe('app', function() {
describe.onServer('.middleware(phase, handler)', function() {
var app;
var steps;
beforeEach(function setup() {
app = loopback();
steps = [];
});
it('runs middleware in phases', function(done) {
var PHASES = [
'initial', 'session', 'auth', 'parse',
'routes', 'files', 'final'
];
PHASES.forEach(function(name) {
app.middleware(name, namedHandler(name));
});
app.use(namedHandler('main'));
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
expect(steps).to.eql([
'initial', 'session', 'auth', 'parse',
'main', 'routes', 'files', 'final'
]);
done();
});
});
it('supports "before:" and "after:" prefixes', function(done) {
app.middleware('routes:before', namedHandler('routes:before'));
app.middleware('routes:after', namedHandler('routes:after'));
app.use(namedHandler('main'));
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
expect(steps).to.eql(['routes:before', 'main', 'routes:after']);
done();
});
});
it('injects error from previous phases into the router', function(done) {
var expectedError = new Error('expected error');
app.middleware('initial', function(req, res, next) {
steps.push('initial');
next(expectedError);
});
// legacy solution for error handling
app.use(function errorHandler(err, req, res, next) {
expect(err).to.equal(expectedError);
steps.push('error');
next();
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
expect(steps).to.eql(['initial', 'error']);
done();
});
});
it('passes unhandled error to callback', function(done) {
var expectedError = new Error('expected error');
app.middleware('initial', function(req, res, next) {
next(expectedError);
});
executeMiddlewareHandlers(app, function(err) {
expect(err).to.equal(expectedError);
done();
});
});
it('passes errors to error handlers in the same phase', function(done) {
var expectedError = new Error('this should be handled by middleware');
var handledError;
app.middleware('initial', function(req, res, next) {
// continue in the next tick, this verifies that the next
// handler waits until the previous one is done
process.nextTick(function() {
next(expectedError);
});
});
app.middleware('initial', function(err, req, res, next) {
handledError = err;
next();
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
expect(handledError).to.equal(expectedError);
done();
});
});
it('scopes middleware to a string path', function(done) {
app.middleware('initial', '/scope', pathSavingHandler());
async.eachSeries(
['/', '/scope', '/scope/item', '/other'],
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
expect(steps).to.eql(['/scope', '/scope/item']);
done();
});
});
it('scopes middleware to a regex path', function(done) {
app.middleware('initial', /^\/(a|b)/, pathSavingHandler());
async.eachSeries(
['/', '/a', '/b', '/c'],
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
expect(steps).to.eql(['/a', '/b']);
done();
});
});
it('scopes middleware to a list of scopes', function(done) {
app.middleware('initial', ['/scope', /^\/(a|b)/], pathSavingHandler());
async.eachSeries(
['/', '/a', '/b', '/c', '/scope', '/other'],
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
expect(steps).to.eql(['/a', '/b', '/scope']);
done();
});
});
function namedHandler(name) {
return function(req, res, next) {
steps.push(name);
next();
};
}
function pathSavingHandler() {
return function(req, res, next) {
steps.push(req.url);
next();
};
}
});
describe.onServer('.middlewareFromConfig', function() {
it('provides API for loading middleware from JSON config', function(done) {
var steps = [];
var expectedConfig = { key: 'value' };
var handlerFactory = function() {
var args = Array.prototype.slice.apply(arguments);
return function(req, res, next) {
steps.push(args);
next();
};
};
// Config as an object (single arg)
app.middlewareFromConfig(handlerFactory, {
enabled: true,
phase: 'session',
params: expectedConfig
});
// Config as a value (single arg)
app.middlewareFromConfig(handlerFactory, {
enabled: true,
phase: 'session:before',
params: 'before'
});
// Config as a list of args
app.middlewareFromConfig(handlerFactory, {
enabled: true,
phase: 'session:after',
params: ['after', 2]
});
// Disabled by configuration
app.middlewareFromConfig(handlerFactory, {
enabled: false,
phase: 'initial',
params: null
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
expect(steps).to.eql([
['before'],
[expectedConfig],
['after', 2]
]);
done();
});
});
it('scopes middleware to a list of scopes', function(done) {
var steps = [];
app.middlewareFromConfig(
function factory() {
return function(req, res, next) {
steps.push(req.url);
next();
};
},
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
async.eachSeries(
['/', '/a', '/b', '/c', '/scope', '/other'],
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
expect(steps).to.eql(['/a', '/b', '/scope']);
done();
});
});
});
describe.onServer('.defineMiddlewarePhases(nameOrArray)', function() {
var app;
beforeEach(function() {
app = loopback();
});
it('adds the phase just before "routes" by default', function(done) {
app.defineMiddlewarePhases('custom');
verifyMiddlewarePhases(['custom', 'routes'], done);
});
it('merges phases adding to the start of the list', function(done) {
app.defineMiddlewarePhases(['first', 'routes', 'subapps']);
verifyMiddlewarePhases([
'first',
'initial', // this was the original first phase
'routes',
'subapps'
], done);
});
it('merges phases preserving the order', function(done) {
app.defineMiddlewarePhases([
'initial',
'postinit', 'preauth', // add
'auth', 'routes',
'subapps', // add
'final',
'last' // add
]);
verifyMiddlewarePhases([
'initial',
'postinit', 'preauth', // new
'auth', 'routes',
'subapps', // new
'files', 'final',
'last' // new
], done);
});
it('throws helpful error on ordering conflict', function() {
app.defineMiddlewarePhases(['first', 'second']);
expect(function() { app.defineMiddlewarePhases(['second', 'first']); })
.to.throw(/ordering conflict.*first.*second/);
});
function verifyMiddlewarePhases(names, done) {
var steps = [];
names.forEach(function(it) {
app.middleware(it, function(req, res, next) {
steps.push(it);
next();
});
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
expect(steps).to.eql(names);
done();
});
}
});
describe('app.model(Model)', function() { describe('app.model(Model)', function() {
var app, db; var app, db;
@ -281,7 +577,7 @@ describe('app', function() {
var elapsed = Date.now() - Number(new Date(res.body.started)); var elapsed = Date.now() - Number(new Date(res.body.started));
// elapsed should be a positive number... // elapsed should be a positive number...
assert(elapsed > 0); assert(elapsed >= 0);
// less than 100 milliseconds // less than 100 milliseconds
assert(elapsed < 100); assert(elapsed < 100);
@ -374,3 +670,20 @@ describe('app', function() {
}); });
}); });
}); });
function executeMiddlewareHandlers(app, urlPath, callback) {
var server = http.createServer(function(req, res) {
app.handle(req, res, callback);
});
if (callback === undefined && typeof urlPath === 'function') {
callback = urlPath;
urlPath = '/test/url';
}
request(server)
.get(urlPath)
.end(function(err) {
if (err) return callback(err);
});
}

View File

@ -1,3 +1,5 @@
var it = require('./util/it');
describe('loopback', function() { describe('loopback', function() {
var nameCounter = 0; var nameCounter = 0;
var uniqueModelName; var uniqueModelName;
@ -11,6 +13,17 @@ describe('loopback', function() {
expect(loopback.ValidationError).to.be.a('function') expect(loopback.ValidationError).to.be.a('function')
.and.have.property('name', 'ValidationError'); .and.have.property('name', 'ValidationError');
}); });
it.onServer('includes `faviconFile`', function() {
var file = loopback.faviconFile;
expect(file, 'faviconFile').to.not.equal(undefined);
expect(require('fs').existsSync(loopback.faviconFile), 'file exists')
.to.equal(true);
});
it.onServer('has `getCurrentContext` method', function() {
expect(loopback.getCurrentContext).to.be.a('function');
});
}); });
describe('loopback.createDataSource(options)', function() { describe('loopback.createDataSource(options)', function() {
@ -68,11 +81,11 @@ describe('loopback', function() {
describe('loopback.remoteMethod(Model, fn, [options]);', function() { describe('loopback.remoteMethod(Model, fn, [options]);', function() {
it("Setup a remote method.", function() { it("Setup a remote method.", function() {
var Product = loopback.createModel('product', {price: Number}); var Product = loopback.createModel('product', {price: Number});
Product.stats = function(fn) { Product.stats = function(fn) {
// ... // ...
} }
loopback.remoteMethod( loopback.remoteMethod(
Product.stats, Product.stats,
{ {
@ -80,7 +93,7 @@ describe('loopback', function() {
http: {path: '/info', verb: 'get'} http: {path: '/info', verb: 'get'}
} }
); );
assert.equal(Product.stats.returns.arg, 'stats'); assert.equal(Product.stats.returns.arg, 'stats');
assert.equal(Product.stats.returns.type, 'array'); assert.equal(Product.stats.returns.type, 'array');
assert.equal(Product.stats.http.path, '/info'); assert.equal(Product.stats.http.path, '/info');
@ -240,9 +253,119 @@ describe('loopback', function() {
expect(owner, 'model.prototype.owner').to.be.a('function'); expect(owner, 'model.prototype.owner').to.be.a('function');
expect(owner._targetClass).to.equal('User'); expect(owner._targetClass).to.equal('User');
}); });
it('adds new acls', function() {
var model = loopback.Model.extend(uniqueModelName, {}, {
acls: [
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: '$everyone',
permission: 'DENY'
}
]
});
loopback.configureModel(model, {
dataSource: null,
acls: [
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: 'admin',
permission: 'ALLOW'
}
]
});
expect(model.settings.acls).eql([
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: '$everyone',
permission: 'DENY'
},
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: 'admin',
permission: 'ALLOW'
}
]);
});
it('updates existing acls', function() {
var model = loopback.Model.extend(uniqueModelName, {}, {
acls: [
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: '$everyone',
permission: 'DENY'
}
]
});
loopback.configureModel(model, {
dataSource: null,
acls: [
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: '$everyone',
permission: 'ALLOW'
}
]
});
expect(model.settings.acls).eql([
{
property: 'find',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: '$everyone',
permission: 'ALLOW'
}
]);
});
it('updates existing settings', function() {
var model = loopback.Model.extend(uniqueModelName, {}, {
ttl: 10,
emailVerificationRequired: false
});
loopback.configureModel(model, {
dataSource: null,
options: {
ttl: 20,
realmRequired: true,
base: 'X'
}
});
expect(model.settings).to.have.property('ttl', 20);
expect(model.settings).to.have.property('emailVerificationRequired',
false);
expect(model.settings).to.have.property('realmRequired', true);
expect(model.settings).to.not.have.property('base');
});
}); });
describe('loopback object', function() { describe('loopback object', function() {
it('inherits properties from express', function() {
var express = require('express');
for (var i in express) {
expect(loopback).to.have.property(i, express[i]);
}
});
it('exports all built-in models', function() { it('exports all built-in models', function() {
var expectedModelNames = [ var expectedModelNames = [
'Email', 'Email',

View File

@ -411,8 +411,8 @@ describe('relations - integration', function () {
}); });
lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/rel/:fk', function () { lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/rel/:fk', function () {
it('should succeed with statusCode 200', function () { it('should succeed with statusCode 204', function () {
assert.equal(this.res.statusCode, 200); assert.equal(this.res.statusCode, 204);
}); });
it('should remove the record in appointment', function (done) { it('should remove the record in appointment', function (done) {
@ -469,8 +469,8 @@ describe('relations - integration', function () {
}); });
lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/:fk', function () { lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/:fk', function () {
it('should succeed with statusCode 200', function () { it('should succeed with statusCode 204', function () {
assert.equal(this.res.statusCode, 200); assert.equal(this.res.statusCode, 204);
}); });
it('should remove the record in appointment', function (done) { it('should remove the record in appointment', function (done) {

View File

@ -66,33 +66,176 @@ describe('remoting - integration', function () {
}); });
}); });
describe('Model', function() { describe('Model shared classes', function() {
it('has expected remote methods', function() { function formatReturns(m) {
var storeClass = app.handler('rest').adapter var returns = m.returns;
if (!returns || returns.length === 0) {
return '';
}
var type = returns[0].type;
return type ? ':' + type : '';
}
function formatMethod(m) {
return [
m.name,
'(',
m.accepts.map(function(a) {
return a.arg + ':' + a.type
}).join(','),
')',
formatReturns(m),
' ',
m.getHttpMethod(),
' ',
m.getFullPath()
].join('');
}
function findClass(name) {
return app.handler('rest').adapter
.getClasses() .getClasses()
.filter(function(c) { return c.name === 'store'; })[0]; .filter(function(c) {
return c.name === name;
})[0];
}
it('has expected remote methods', function() {
var storeClass = findClass('store');
var methods = storeClass.methods var methods = storeClass.methods
.filter(function(m) {
return m.name.indexOf('__') === -1;
})
.map(function(m) { .map(function(m) {
return [ return formatMethod(m);
m.name + '()',
m.getHttpMethod(),
m.getFullPath()
].join(' ');
}); });
var expectedMethods = [
'create(data:object):store POST /stores',
'upsert(data:object):store PUT /stores',
'exists(id:any):boolean GET /stores/:id/exists',
'findById(id:any):store GET /stores/:id',
'find(filter:object):store GET /stores',
'findOne(filter:object):store GET /stores/findOne',
'updateAll(where:object,data:object) POST /stores/update',
'deleteById(id:any) DELETE /stores/:id',
'count(where:object):number GET /stores/count',
'prototype.updateAttributes(data:object):store PUT /stores/:id'
];
// The list of methods is from docs: // The list of methods is from docs:
// http://docs.strongloop.com/display/LB/Exposing+models+over+a+REST+API // http://docs.strongloop.com/display/LB/Exposing+models+over+a+REST+API
expect(methods).to.include.members([ expect(methods).to.include.members(expectedMethods);
'create() POST /stores', });
'upsert() PUT /stores',
'exists() GET /stores/:id/exists', it('has expected remote methods for scopes', function() {
'findById() GET /stores/:id', var storeClass = findClass('store');
'find() GET /stores', var methods = storeClass.methods
'findOne() GET /stores/findOne', .filter(function(m) {
'deleteById() DELETE /stores/:id', return m.name.indexOf('__') === 0;
'count() GET /stores/count', })
'prototype.updateAttributes() PUT /stores/:id', .map(function(m) {
]); return formatMethod(m);
});
var expectedMethods = [
'__get__superStores(filter:object):store GET /stores/superStores',
'__create__superStores(data:store):store POST /stores/superStores',
'__delete__superStores() DELETE /stores/superStores',
'__count__superStores(where:object):number GET /stores/superStores/count'
];
expect(methods).to.include.members(expectedMethods);
});
it('should have correct signatures for belongsTo methods',
function() {
var widgetClass = findClass('widget');
var methods = widgetClass.methods
.filter(function(m) {
return m.name.indexOf('prototype.__') === 0;
})
.map(function(m) {
return formatMethod(m);
});
var expectedMethods = [
'prototype.__get__store(refresh:boolean):store ' +
'GET /widgets/:id/store'
];
expect(methods).to.include.members(expectedMethods);
});
it('should have correct signatures for hasMany methods',
function() {
var physicianClass = findClass('store');
var methods = physicianClass.methods
.filter(function(m) {
return m.name.indexOf('prototype.__') === 0;
})
.map(function(m) {
return formatMethod(m);
});
var expectedMethods = [
'prototype.__findById__widgets(fk:any):widget ' +
'GET /stores/:id/widgets/:fk',
'prototype.__destroyById__widgets(fk:any) ' +
'DELETE /stores/:id/widgets/:fk',
'prototype.__updateById__widgets(fk:any,data:widget):widget ' +
'PUT /stores/:id/widgets/:fk',
'prototype.__get__widgets(filter:object):widget ' +
'GET /stores/:id/widgets',
'prototype.__create__widgets(data:widget):widget ' +
'POST /stores/:id/widgets',
'prototype.__delete__widgets() ' +
'DELETE /stores/:id/widgets',
'prototype.__count__widgets(where:object):number ' +
'GET /stores/:id/widgets/count'
];
expect(methods).to.include.members(expectedMethods);
});
it('should have correct signatures for hasMany-through methods',
function() {
var physicianClass = findClass('physician');
var methods = physicianClass.methods
.filter(function(m) {
return m.name.indexOf('prototype.__') === 0;
})
.map(function(m) {
return formatMethod(m);
});
var expectedMethods = [
'prototype.__findById__patients(fk:any):patient ' +
'GET /physicians/:id/patients/:fk',
'prototype.__destroyById__patients(fk:any) ' +
'DELETE /physicians/:id/patients/:fk',
'prototype.__updateById__patients(fk:any,data:patient):patient ' +
'PUT /physicians/:id/patients/:fk',
'prototype.__link__patients(fk:any,data:appointment):appointment ' +
'PUT /physicians/:id/patients/rel/:fk',
'prototype.__unlink__patients(fk:any) ' +
'DELETE /physicians/:id/patients/rel/:fk',
'prototype.__exists__patients(fk:any):boolean ' +
'HEAD /physicians/:id/patients/rel/:fk',
'prototype.__get__patients(filter:object):patient ' +
'GET /physicians/:id/patients',
'prototype.__create__patients(data:patient):patient ' +
'POST /physicians/:id/patients',
'prototype.__delete__patients() ' +
'DELETE /physicians/:id/patients',
'prototype.__count__patients(where:object):number ' +
'GET /physicians/:id/patients/count'
];
expect(methods).to.include.members(expectedMethods);
}); });
}); });
}); });

View File

@ -75,6 +75,22 @@ describe('loopback.rest', function() {
}); });
}); });
it('should honour `remoting.rest.supportedTypes`', function(done) {
var app = loopback();
// NOTE it is crucial to set `remoting` before creating any models
var supportedTypes = ['json', 'application/javascript', 'text/javascript'];
app.set('remoting', { rest: { supportedTypes: supportedTypes } });
app.model(MyModel);
app.use(loopback.rest());
request(app).get('/mymodels')
.set('Accept', 'text/html,application/xml;q=0.9,*/*;q=0.8')
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200, done);
});
it('includes loopback.token when necessary', function(done) { it('includes loopback.token when necessary', function(done) {
givenUserModelWithAuth(); givenUserModelWithAuth();
app.enableAuth(); app.enableAuth();
@ -114,6 +130,107 @@ describe('loopback.rest', function() {
}); });
}); });
describe('context propagation', function() {
var User;
beforeEach(function() {
User = givenUserModelWithAuth();
User.getToken = function(cb) {
var context = loopback.getCurrentContext();
var req = context.get('http').req;
expect(req).to.have.property('accessToken');
var juggler = require('loopback-datasource-juggler');
expect(juggler.getCurrentContext().get('http').req)
.to.have.property('accessToken');
var remoting = require('strong-remoting');
expect(remoting.getCurrentContext().get('http').req)
.to.have.property('accessToken');
cb(null, req && req.accessToken ? req.accessToken.id : null);
};
// Set up the ACL
User.settings.acls.push({principalType: 'ROLE',
principalId: '$authenticated', permission: 'ALLOW',
property: 'getToken'});
loopback.remoteMethod(User.getToken, {
accepts: [],
returns: [
{ type: 'object', name: 'id' }
]
});
});
function invokeGetToken(done) {
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(token.id);
done();
});
});
}
it('should enable context using loopback.context', function(done) {
app.use(loopback.context({ enableHttpContext: true }));
app.enableAuth();
app.use(loopback.rest());
invokeGetToken(done);
});
it('should enable context with loopback.rest', function(done) {
app.enableAuth();
app.set('remoting', { context: { enableHttpContext: true } });
app.use(loopback.rest());
invokeGetToken(done);
});
it('should support explicit context', function(done) {
app.enableAuth();
app.use(loopback.context());
app.use(loopback.token(
{ model: loopback.getModelByType(loopback.AccessToken) }));
app.use(function(req, res, next) {
loopback.getCurrentContext().set('accessToken', req.accessToken);
next();
});
app.use(loopback.rest());
User.getToken = function(cb) {
var context = loopback.getCurrentContext();
var accessToken = context.get('accessToken');
expect(context.get('accessToken')).to.have.property('id');
var juggler = require('loopback-datasource-juggler');
context = juggler.getCurrentContext();
expect(context.get('accessToken')).to.have.property('id');
var remoting = require('strong-remoting');
context = remoting.getCurrentContext();
expect(context.get('accessToken')).to.have.property('id');
cb(null, accessToken ? accessToken.id : null);
};
loopback.remoteMethod(User.getToken, {
accepts: [],
returns: [
{ type: 'object', name: 'id' }
]
});
invokeGetToken(done);
});
});
function givenUserModelWithAuth() { function givenUserModelWithAuth() {
// NOTE(bajtos) It is important to create a custom AccessToken model here, // NOTE(bajtos) It is important to create a custom AccessToken model here,
// in order to overwrite the entry created by previous tests in // in order to overwrite the entry created by previous tests in