Merge branch 'release/2.8.0' into production
This commit is contained in:
commit
5526402e1c
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
2301
CHANGES.md
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||||
|
|
||||||
|
|
21
Gruntfile.js
21
Gruntfile.js
|
@ -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: {
|
||||||
|
@ -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']);
|
||||||
|
|
||||||
|
|
|
@ -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 || [];
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,12 +25,12 @@ module.exports = function(RoleMapping) {
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -43,12 +43,12 @@ module.exports = function(RoleMapping) {
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -66,7 +66,7 @@ module.exports = function(RoleMapping) {
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)) {
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,7 @@ module.exports = function(Scope) {
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loopback = require('../../lib/loopback')
|
var loopback = require('../../lib/loopback');
|
||||||
, path = require('path')
|
var path = require('path');
|
||||||
, SALT_WORK_FACTOR = 10
|
var SALT_WORK_FACTOR = 10;
|
||||||
, crypto = require('crypto')
|
var crypto = require('crypto');
|
||||||
, bcrypt = require('bcryptjs')
|
var bcrypt = require('bcryptjs');
|
||||||
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
var DEFAULT_TTL = 1209600; // 2 weeks in seconds
|
||||||
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds
|
var DEFAULT_RESET_PW_TTL = 15 * 60; // 15 mins in seconds
|
||||||
, DEFAULT_MAX_TTL = 31556926 // 1 year in seconds
|
var DEFAULT_MAX_TTL = 31556926; // 1 year in seconds
|
||||||
, assert = require('assert');
|
var assert = require('assert');
|
||||||
|
|
||||||
var debug = require('debug')('loopback:user');
|
var debug = require('debug')('loopback:user');
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ module.exports = function(User) {
|
||||||
* Create access token for the logged in user. This method can be overridden to
|
* Create access token for the logged in user. This method can be overridden to
|
||||||
* customize how access tokens are generated
|
* customize how access tokens are generated
|
||||||
*
|
*
|
||||||
* @param [Number} ttl The requested ttl
|
* @param {Number} ttl The requested ttl
|
||||||
* @callack {Function} cb The callback function
|
* @callack {Function} cb The callback function
|
||||||
* @param {String|Error} err The error string or object
|
* @param {String|Error} err The error string or object
|
||||||
* @param {AccessToken} token The generated access token object
|
* @param {AccessToken} token The generated access token object
|
||||||
|
@ -106,7 +106,7 @@ User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return query;
|
return query;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login a user by with the given `credentials`.
|
* Login a user by with the given `credentials`.
|
||||||
|
@ -117,10 +117,12 @@ User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter)
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {Object} credentials
|
* @param {Object} credentials username/password or email/password
|
||||||
* @callback {Function} callback
|
* @param {String[]|String} [include] Optionally set it to "user" to include
|
||||||
* @param {Error} err
|
* the user info
|
||||||
* @param {AccessToken} token
|
* @callback {Function} callback Callback function
|
||||||
|
* @param {Error} err Error object
|
||||||
|
* @param {AccessToken} token Access token if login is successful
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.login = function(credentials, include, fn) {
|
User.login = function(credentials, include, fn) {
|
||||||
|
@ -231,7 +233,7 @@ User.logout = function(tokenId, fn) {
|
||||||
fn(new Error('could not find accessToken'));
|
fn(new Error('could not find accessToken'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare the given `password` with the users hashed password.
|
* Compare the given `password` with the users hashed password.
|
||||||
|
@ -249,7 +251,7 @@ User.prototype.hasPassword = function(plain, fn) {
|
||||||
} else {
|
} else {
|
||||||
fn(null, false);
|
fn(null, false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a user's identity by sending them a confirmation email.
|
* Verify a user's identity by sending them a confirmation email.
|
||||||
|
@ -297,19 +299,18 @@ User.prototype.verify = function(options, fn) {
|
||||||
options.port = options.port || (app && app.get('port')) || 3000;
|
options.port = options.port || (app && app.get('port')) || 3000;
|
||||||
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
|
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
|
||||||
options.verifyHref = options.verifyHref ||
|
options.verifyHref = options.verifyHref ||
|
||||||
options.protocol
|
options.protocol +
|
||||||
+ '://'
|
'://' +
|
||||||
+ options.host
|
options.host +
|
||||||
+ ':'
|
':' +
|
||||||
+ options.port
|
options.port +
|
||||||
+ options.restApiRoot
|
options.restApiRoot +
|
||||||
+ userModel.http.path
|
userModel.http.path +
|
||||||
+ userModel.confirm.http.path
|
userModel.confirm.http.path +
|
||||||
+ '?uid='
|
'?uid=' +
|
||||||
+ options.user.id
|
options.user.id +
|
||||||
+ '&redirect='
|
'&redirect=' +
|
||||||
+ options.redirect;
|
options.redirect;
|
||||||
|
|
||||||
|
|
||||||
// Email model
|
// Email model
|
||||||
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
||||||
|
@ -353,8 +354,7 @@ User.prototype.verify = function(options, fn) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm the user's identity.
|
* Confirm the user's identity.
|
||||||
|
@ -392,7 +392,7 @@ User.confirm = function(uid, token, redirect, fn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a short lived acess token for temporary login. Allows users
|
* Create a short lived acess token for temporary login. Allows users
|
||||||
|
@ -427,7 +427,7 @@ User.resetPassword = function(options, cb) {
|
||||||
user: user
|
user: user
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
@ -438,7 +438,7 @@ User.resetPassword = function(options, cb) {
|
||||||
|
|
||||||
cb(err);
|
cb(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Setup an extended user model.
|
* Setup an extended user model.
|
||||||
|
@ -456,7 +456,7 @@ User.setup = function() {
|
||||||
UserModel.setter.password = function(plain) {
|
UserModel.setter.password = function(plain) {
|
||||||
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
||||||
this.$password = bcrypt.hashSync(plain, salt);
|
this.$password = bcrypt.hashSync(plain, salt);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Make sure emailVerified is not set by creation
|
// Make sure emailVerified is not set by creation
|
||||||
UserModel.beforeRemote('create', function(ctx, user, next) {
|
UserModel.beforeRemote('create', function(ctx, user, next) {
|
||||||
|
@ -473,11 +473,14 @@ User.setup = function() {
|
||||||
description: 'Login a user with username/email and password',
|
description: 'Login a user with username/email and password',
|
||||||
accepts: [
|
accepts: [
|
||||||
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
|
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
|
||||||
{arg: 'include', type: 'string', http: {source: 'query' }, description: 'Related objects to include in the response. ' +
|
{arg: 'include', type: 'string', http: {source: 'query' },
|
||||||
|
description: 'Related objects to include in the response. ' +
|
||||||
'See the description of return value for more details.'}
|
'See the description of return value for more details.'}
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
arg: 'accessToken', type: 'object', root: true, description: 'The response body contains properties of the AccessToken created on login.\n' +
|
arg: 'accessToken', type: 'object', root: true,
|
||||||
|
description:
|
||||||
|
'The response body contains properties of the AccessToken created on login.\n' +
|
||||||
'Depending on the value of `include` parameter, the body may contain ' +
|
'Depending on the value of `include` parameter, the body may contain ' +
|
||||||
'additional properties:\n\n' +
|
'additional properties:\n\n' +
|
||||||
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
||||||
|
@ -549,7 +552,6 @@ User.setup = function() {
|
||||||
// email validation regex
|
// email validation regex
|
||||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
|
|
||||||
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||||
|
|
||||||
// FIXME: We need to add support for uniqueness of composite keys in juggler
|
// FIXME: We need to add support for uniqueness of composite keys in juggler
|
||||||
|
@ -559,7 +561,7 @@ User.setup = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserModel;
|
return UserModel;
|
||||||
}
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Setup the base user.
|
* Setup the base user.
|
||||||
|
|
10
docs.json
10
docs.json
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'});
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
|
@ -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');
|
||||||
|
|
|
@ -126,7 +126,6 @@ AccessContext.prototype.getUserId = function() {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the application id
|
* Get the application id
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
|
@ -149,7 +148,7 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -206,7 +205,7 @@ 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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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).
|
||||||
|
@ -385,9 +385,11 @@ function configureModel(ModelCtor, config, app) {
|
||||||
dataSource = app.dataSources[dataSource];
|
dataSource = app.dataSources[dataSource];
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(dataSource instanceof DataSource,
|
assert(
|
||||||
|
dataSource instanceof DataSource,
|
||||||
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
|
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
|
||||||
config.dataSource +'"');
|
config.dataSource + '"'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config = extend({}, config);
|
config = extend({}, config);
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -160,8 +159,8 @@ Mailer.send = function (options, fn) {
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
var express = require('express');
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
var middlewares = exports;
|
var middlewares = exports;
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -164,6 +189,10 @@ loopback.template = function (file) {
|
||||||
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
|
||||||
|
|
29
lib/model.js
29
lib/model.js
|
@ -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?
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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,7 +79,7 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@ -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) {
|
||||||
* ...
|
* ...
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
|
@ -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'}
|
||||||
|
@ -850,7 +850,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
|
||||||
sourceModel.emit('conflicts', conflicts);
|
sourceModel.emit('conflicts', conflicts);
|
||||||
}
|
}
|
||||||
|
|
||||||
callback && callback(null, conflicts);
|
if (callback) callback(null, conflicts);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -869,7 +869,7 @@ 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) {
|
||||||
|
@ -880,8 +880,8 @@ PersistedModel.createUpdates = function(deltas, cb) {
|
||||||
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();
|
||||||
|
@ -1011,6 +1011,7 @@ PersistedModel.enableChangeTracking = function() {
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
setInterval(cleanup, cleanupInterval);
|
setInterval(cleanup, cleanupInterval);
|
||||||
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
Model.rectifyAllChanges(function(err) {
|
Model.rectifyAllChanges(function(err) {
|
||||||
|
@ -1020,7 +1021,6 @@ PersistedModel.enableChangeTracking = function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
PersistedModel._defineChangeModel = function() {
|
PersistedModel._defineChangeModel = function() {
|
||||||
|
@ -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) {
|
||||||
|
|
|
@ -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: {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -356,9 +414,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,4 +19,3 @@ runtime.isBrowser = typeof window !== 'undefined';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
runtime.isServer = !runtime.isBrowser;
|
runtime.isServer = !runtime.isBrowser;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
18
package.json
18
package.json
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../lib/express-middleware').favicon;
|
|
@ -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.
|
||||||
|
@ -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) {
|
async.eachSeries(preHandlers.concat(restHandler), function(handler, done) {
|
||||||
if (err) {
|
handler(req, res, done);
|
||||||
next(err);
|
}, next);
|
||||||
} else {
|
|
||||||
handler(req, res, next);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handler(req, res, next);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -27,4 +27,3 @@ function status() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -52,8 +52,9 @@ function token(options) {
|
||||||
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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
317
test/app.test.js
317
test/app.test.js
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
@ -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',
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
.getClasses()
|
if (!returns || returns.length === 0) {
|
||||||
.filter(function(c) { return c.name === 'store'; })[0];
|
return '';
|
||||||
var methods = storeClass.methods
|
}
|
||||||
.map(function(m) {
|
var type = returns[0].type;
|
||||||
|
return type ? ':' + type : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMethod(m) {
|
||||||
return [
|
return [
|
||||||
m.name + '()',
|
m.name,
|
||||||
|
'(',
|
||||||
|
m.accepts.map(function(a) {
|
||||||
|
return a.arg + ':' + a.type
|
||||||
|
}).join(','),
|
||||||
|
')',
|
||||||
|
formatReturns(m),
|
||||||
|
' ',
|
||||||
m.getHttpMethod(),
|
m.getHttpMethod(),
|
||||||
|
' ',
|
||||||
m.getFullPath()
|
m.getFullPath()
|
||||||
].join('');
|
].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClass(name) {
|
||||||
|
return app.handler('rest').adapter
|
||||||
|
.getClasses()
|
||||||
|
.filter(function(c) {
|
||||||
|
return c.name === name;
|
||||||
|
})[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
it('has expected remote methods', function() {
|
||||||
|
var storeClass = findClass('store');
|
||||||
|
var methods = storeClass.methods
|
||||||
|
.filter(function(m) {
|
||||||
|
return m.name.indexOf('__') === -1;
|
||||||
|
})
|
||||||
|
.map(function(m) {
|
||||||
|
return formatMethod(m);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue