Merge branch 'release/2.14.0' into production

This commit is contained in:
Raymond Feng 2015-01-14 12:44:24 -08:00
commit abd0d244cc
13 changed files with 249 additions and 76 deletions

View File

@ -1,3 +1,23 @@
2015-01-14, Version 2.14.0
==========================
* Remove console.log (Raymond Feng)
* Fix for #369 (Dallon Feldner)
* Fix virtual id get function. (Berkeley Martinez)
* Fix Model.prototype.inspect (Miroslav Bajtoš)
* Include property value in the error message (Miroslav Bajtoš)
* Update datasource.js (Rand McKinney)
* Change Model to BaseModel for clarity (Fabien Franzen)
* Don't coerce nested objects into Model instances (Fabien Franzen)
2015-01-07, Version 2.13.0
==========================
@ -368,13 +388,6 @@
* Properly handle LDL for polymorphic relations (Fabien Franzen)
* Check null (Raymond Feng)
2014-08-15, Version 2.4.0
=========================
2014-08-15, Version 2.4.1
=========================
@ -383,6 +396,12 @@
* Check null (Raymond Feng)
2014-08-15, Version 2.4.0
=========================
* Bump version (Raymond Feng)
* Fix the test cases to avoid hard-coded ids (Raymond Feng)
* Add strict flag to sortObjectsByIds (Fabien Franzen)
@ -429,16 +448,19 @@
* Cleanup mixin tests (Fabien Franzen)
2014-08-08, Version 2.3.1
=========================
* Fix a name conflict in scope metadata (Raymond Feng)
2014-08-08, Version 2.3.0
=========================
2014-08-08, Version 2.3.1
=========================
* Fix a name conflict in scope metadata (Raymond Feng)
* Fix the test case so that it works with other DBs (Raymond Feng)
* Bump version (Raymond Feng)

View File

@ -436,7 +436,7 @@ function applyFilter(filter) {
return undefined;
}
if (typeof example === 'object') {
if (typeof example === 'object' && example !== null) {
// ignore geo near filter
if (example.near) {
return true;

View File

@ -21,6 +21,7 @@ var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var mergeQuery = utils.mergeQuery;
var util = require('util');
var assert = require('assert');
var BaseModel = require('./model');
/**
* Base class for all persistent objects.
@ -593,6 +594,10 @@ DataAccessObject._coerce = function (where) {
continue;
}
if (DataType.prototype instanceof BaseModel) {
continue;
}
if (DataType === geo.GeoPoint) {
// Skip the GeoPoint as the near operator breaks the assumption that
// an operation has only one property

View File

@ -65,7 +65,7 @@ var slice = Array.prototype.slice;
* // work with database
* });
* ```
* @class Define new DataSource
* @class DataSource
* @param {String} name Type of dataSource connector (mysql, mongoose, oracle, redis)
* @param {Object} settings Database-specific settings to establish connection (settings depend on specific connector). See above.
*/

View File

@ -253,7 +253,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
if (idProp !== 'id') {
Object.defineProperty(ModelClass.prototype, 'id', {
get: function () {
var idProp = ModelClass.definition.idNames[0];
var idProp = ModelClass.definition.idNames()[0];
return this.__data[idProp];
},
configurable: true,

View File

@ -438,8 +438,28 @@ ModelBaseClass.prototype.reset = function () {
}
};
ModelBaseClass.prototype.inspect = function () {
return util.inspect(this.__data, false, 4, true);
// Node v0.11+ allows custom inspect functions to return an object
// instead of string. That way options like `showHidden` and `colors`
// can be preserved.
var versionParts = process.versions.node
.split(/\./g).map(function(v) { return +v; });
var INSPECT_SUPPORTS_OBJECT_RETVAL =
versionParts[0] > 0 ||
versionParts[1] > 11 ||
(versionParts[0] === 11 && versionParts[1] >= 14);
ModelBaseClass.prototype.inspect = function (depth) {
if (INSPECT_SUPPORTS_OBJECT_RETVAL)
return this.__data;
// Workaround for older versions
// See also https://github.com/joyent/node/commit/66280de133
return util.inspect(this.__data, {
showHidden: false,
depth: depth,
colors: false
});
};
ModelBaseClass.mixin = function (anotherClass, options) {

View File

@ -735,7 +735,7 @@ function ValidationError(obj) {
this.message = util.format(
'The %s instance is not valid. Details: %s.',
context ? '`' + context + '`' : 'model',
formatErrors(obj.errors) || '(unknown)'
formatErrors(obj.errors, obj.toJSON()) || '(unknown)'
);
this.statusCode = 422;
@ -761,7 +761,9 @@ util.inherits(ValidationError, Error);
var errorHasStackProperty = !!(new Error).stack;
function formatErrors(errors) {
ValidationError.maxPropertyStringLength = 32;
function formatErrors(errors, propertyValues) {
var DELIM = '; ';
errors = errors || {};
return Object.getOwnPropertyNames(errors)
@ -770,9 +772,52 @@ function formatErrors(errors) {
})
.map(function(propertyName) {
var messages = errors[propertyName];
var propertyValue = propertyValues[propertyName];
return messages.map(function(msg) {
return '`' + propertyName + '` ' + msg;
return formatPropertyError(propertyName, propertyValue, msg);
}).join(DELIM);
})
.join(DELIM);
}
function formatPropertyError(propertyName, propertyValue, errorMessage) {
var formattedValue;
var valueType = typeof propertyValue;
if (valueType === 'string') {
formattedValue = JSON.stringify(truncatePropertyString(propertyValue));
} else if (propertyValue instanceof Date) {
formattedValue = propertyValue.toISOString();
} else if (valueType === 'object') {
// objects and arrays
formattedValue = util.inspect(propertyValue, {
showHidden: false,
color: false,
// show top-level object properties only
depth: Array.isArray(propertyValue) ? 1 : 0
});
formattedValue = truncatePropertyString(formattedValue);
} else {
formattedValue = truncatePropertyString('' + propertyValue);
}
return '`' + propertyName + '` ' + errorMessage +
' (value: ' + formattedValue + ')';
}
function truncatePropertyString(value) {
var len = ValidationError.maxPropertyStringLength;
if (value.length <= len) return value;
// preserve few last characters like `}` or `]`, but no more than 3
// this way the last `} ]` in the array of objects is included in the message
var tail;
var m = value.match(/([ \t})\]]+)$/);
if (m) {
tail = m[1].slice(-3);
len -= tail.length;
} else {
tail = value.slice(-3);
len -= 3;
}
return value.slice(0, len-4) + '...' + tail;
}

View File

@ -1,6 +1,6 @@
{
"name": "loopback-datasource-juggler",
"version": "2.13.0",
"version": "2.14.0",
"description": "LoopBack DataSoure Juggler",
"keywords": [
"StrongLoop",

View File

@ -7,13 +7,16 @@ describe('datatypes', function () {
before(function (done) {
db = getSchema();
Nested = db.define('Nested', {});
Model = db.define('Model', {
str: String,
date: Date,
num: Number,
bool: Boolean,
list: {type: [String]},
arr: Array
arr: Array,
nested: Nested
});
db.automigrate(function () {
Model.destroyAll(done);
@ -114,4 +117,10 @@ describe('datatypes', function () {
});
}
});
it('should not coerce nested objects into ModelConstructor types', function() {
var coerced = Model._coerce({ nested: { foo: 'bar' } });
coerced.nested.constructor.name.should.equal('Object');
});
});

View File

@ -66,8 +66,8 @@ describe('manipulation', function () {
it('should not allow user-defined value for the id of object - create', function (done) {
Person.create({id: 123456}, function (err, p) {
err.should.be.instanceof(ValidationError);
err.message.should.equal('The `Person` instance is not valid. Details: `id` can\'t be set.');
err.statusCode.should.equal(422);
err.details.messages.id.should.eql(['can\'t be set']);
p.should.be.instanceof(Person);
p.id.should.equal(123456);
p.isNewRecord().should.be.true;
@ -80,8 +80,8 @@ describe('manipulation', function () {
p.isNewRecord().should.be.true;
p.save(function(err, inst) {
err.should.be.instanceof(ValidationError);
err.message.should.equal('The `Person` instance is not valid. Details: `id` can\'t be set.');
err.statusCode.should.equal(422);
err.details.messages.id.should.eql(['can\'t be set']);
inst.id.should.equal(123456);
inst.isNewRecord().should.be.true;
done();

View File

@ -194,7 +194,6 @@ describe('Memory connector', function () {
it('should sort undefined values to the end when ordered DESC', function (done) {
User.find({order: 'vip ASC, order DESC'}, function (err, posts) {
console.log(posts);
should.not.exist(err);
posts[4].seq.should.be.eql(1);

View File

@ -2111,9 +2111,7 @@ describe('relations', function () {
p.passportItem.create({}, function(err, passport) {
should.exist(err);
err.name.should.equal('ValidationError');
var msg = 'The `Passport` instance is not valid.';
msg += ' Details: `name` can\'t be blank.';
err.message.should.equal(msg);
err.details.messages.name.should.eql(['can\'t be blank']);
done();
});
});
@ -2125,9 +2123,8 @@ describe('relations', function () {
p.save(function(err) {
should.exist(err);
err.name.should.equal('ValidationError');
var msg = 'The `Person` instance is not valid.';
msg += ' Details: `passportItem` is invalid: `name` can\'t be blank.';
err.message.should.equal(msg);
err.details.messages.passportItem
.should.eql(['is invalid: `name` can\'t be blank']);
done();
});
});
@ -2568,9 +2565,9 @@ describe('relations', function () {
addresses.push({ id: 'work', street: '' });
Person.create({ name: 'Wilma', addresses: addresses }, function(err, p) {
err.name.should.equal('ValidationError');
var expected = 'The `Person` instance is not valid. ';
expected += 'Details: `addresses` contains invalid item: `work` (`street` can\'t be blank).';
err.message.should.equal(expected);
err.details.messages.addresses.should.eql([
'contains invalid item: `work` (`street` can\'t be blank)'
]);
done();
});
});
@ -2769,7 +2766,7 @@ describe('relations', function () {
err.name.should.equal('ValidationError');
err.details.codes.street.should.eql(['presence']);
var expected = 'The `Address` instance is not valid. ';
expected += 'Details: `street` can\'t be blank.';
expected += 'Details: `street` can\'t be blank (value: undefined).';
err.message.should.equal(expected);
done();
});
@ -3225,9 +3222,6 @@ describe('relations', function () {
should.exist(err);
err.name.should.equal('ValidationError');
err.details.codes.jobs.should.eql(['uniqueness']);
var expected = 'The `Category` instance is not valid. ';
expected += 'Details: `jobs` contains duplicate `Job` instance.';
err.message.should.equal(expected);
done();
});
});

View File

@ -211,6 +211,16 @@ describe('validations', function () {
});
});
it('should include property value in err.message', function(done) {
delete User.validations;
User.validatesPresenceOf('name');
User.create(function (e, u) {
should.exist(e);
e.message.should.match(/`name` can't be blank \(value: undefined\)/);
done();
});
});
it('should include model name in err.message', function(done) {
delete User.validations;
User.validatesPresenceOf('name');
@ -432,4 +442,73 @@ describe('validations', function () {
})).should.be.false;
});
});
describe('invalid value formatting', function() {
var origMaxLen;
beforeEach(function saveAndSetMaxLen() {
origMaxLen = ValidationError.maxPropertyStringLength;
});
afterEach(function restoreMaxLen() {
ValidationError.maxPropertyStringLength = origMaxLen;
});
it('should truncate long strings', function() {
ValidationError.maxPropertyStringLength = 9;
var err = givenValidationError('prop', '1234567890abc', 'is invalid');
getErrorDetails(err)
.should.equal('`prop` is invalid (value: "12...abc").');
});
it('should truncate long objects', function() {
ValidationError.maxPropertyStringLength = 12;
var err = givenValidationError('prop', { foo: 'bar' }, 'is invalid');
getErrorDetails(err)
.should.equal('`prop` is invalid (value: { foo:... }).');
});
it('should truncate long arrays', function() {
ValidationError.maxPropertyStringLength = 12;
var err = givenValidationError('prop', [{ a: 1, b: 2}], 'is invalid');
getErrorDetails(err)
.should.equal('`prop` is invalid (value: [ { a...} ]).');
});
it('should print only top-level object properties', function() {
var err = givenValidationError('prop', { a: { b: 'c' }}, 'is invalid');
getErrorDetails(err)
.should.equal('`prop` is invalid (value: { a: [Object] }).');
});
it('should print only top-level props of objects in array', function() {
var err = givenValidationError('prop', [{ a: { b: 'c' }}], 'is invalid');
getErrorDetails(err)
.should.equal('`prop` is invalid (value: [ { a: [Object] } ]).');
});
it('should exclude colors from Model values', function() {
var obj = new User();
obj.email = 'test@example.com';
var err = givenValidationError('user', obj, 'is invalid');
getErrorDetails(err).should.equal(
'`user` is invalid (value: { email: \'test@example.com\' }).');
});
function givenValidationError(propertyName, propertyValue, errorMessage) {
var jsonVal = {};
jsonVal[propertyName] = propertyValue;
var errorVal = {};
errorVal[propertyName] = [errorMessage];
var obj = {
errors: errorVal,
toJSON: function() { return jsonVal; }
};
return new ValidationError(obj);
}
function getErrorDetails(err) {
return err.message.replace(/^.*Details: /, '');
}
});
});