promised-models2
Advanced tools
Comparing version 0.1.0 to 0.1.1
var Inheritable = require('./inheritable'); | ||
var Inheritable = require('./inheritable'), | ||
Vow = require('vow'), | ||
fulfill = require('./fulfill'); | ||
@@ -52,9 +54,22 @@ /** | ||
* @abstract | ||
* @return {Boolean|Promise<{Boolean}>} | ||
* @return {Promise} | ||
*/ | ||
validate: function () { | ||
return true; | ||
var error = this.getValidationError(); | ||
if (!error) { | ||
return fulfill(true); | ||
} else { | ||
return Vow.reject(error); | ||
} | ||
}, | ||
/** | ||
* Helper method for attribute validation. | ||
* Attribute is not valid if it will return non-falsy value | ||
* @abstract | ||
* @returns {*} | ||
*/ | ||
getValidationError: function () { }, | ||
/** | ||
* return serializable value of attribute | ||
@@ -220,4 +235,21 @@ * @return {*} | ||
}, { | ||
ValidationError: (function () { | ||
/** | ||
* @param {String} message | ||
*/ | ||
var ValidationError = function (message) { | ||
this.name = 'AttributeValidationError'; | ||
this.message = message; | ||
Error.call(this); | ||
Error.captureStackTrace(this, this.constructor); | ||
}; | ||
ValidationError.prototype = Object.create(Error.prototype); | ||
ValidationError.prototype.constructor = ValidationError; | ||
return ValidationError; | ||
})() | ||
}); | ||
module.exports = Attribute; |
@@ -9,2 +9,3 @@ /** | ||
IdAttribute = require('./types/id'), | ||
Attribute = require('./attribute'), | ||
fulfill = require('./fulfill'), | ||
@@ -212,3 +213,3 @@ | ||
* check if model is valid | ||
* @return {Promise<, {Model.ValidationError}>} | ||
* @return {Promise<Boolean, Model.ValidationError>} | ||
*/ | ||
@@ -218,22 +219,40 @@ validate: function () { | ||
return model.ready().then(function () { | ||
return Vow.all(model._attributesAr.map(function (attribute) { | ||
return Vow.allResolved(model._attributesAr.map(function (attribute) { | ||
return attribute.validate(); | ||
})); | ||
}).then(function (validateResults) { | ||
var isValid = validateResults.every(function(result) { | ||
return result === true; | ||
}).then(function (validationPromises) { | ||
var errors = [], | ||
error; | ||
validationPromises.forEach(function (validationPromise, index) { | ||
var validationResult, error; | ||
if (validationPromise.isFulfilled()) { | ||
return; | ||
} | ||
validationResult = validationPromise.valueOf(); | ||
if (validationResult instanceof Error) { | ||
error = validationResult; | ||
} else { | ||
error = new Attribute.ValidationError(); | ||
error.attribute = model._attributesAr[index]; | ||
if (typeof validationResult === 'string') { | ||
error.message = validationResult; | ||
} else if (typeof validationResult !== 'boolean') { | ||
error.data = validationResult; | ||
} | ||
} | ||
errors.push(error); | ||
}); | ||
if (isValid) { | ||
if (errors.length) { | ||
error = new model.__self.ValidationError(); | ||
error.attributes = errors; | ||
return Vow.reject(error); | ||
} else { | ||
return fulfill(true); | ||
} else { | ||
return Vow.reject(validateResults.reduce(function (err, validationResult, k) { | ||
if (validationResult !== true) { | ||
if (validationResult === false) { | ||
validationResult = new model.__self.ValidationAttributeError(model._attributesAr[k]); | ||
} | ||
err.attributes.push(validationResult); | ||
} | ||
return err; | ||
}, new model.__self.ValidationError())); | ||
} | ||
@@ -604,18 +623,2 @@ }); | ||
/** | ||
* @param {Attribute} attribute | ||
*/ | ||
ValidationAttributeError: (function () { | ||
var ValidationAttributeError = function (attribute, type) { | ||
this.name = 'ValidationAttributeError'; | ||
this.attribute = attribute; | ||
this.type = type; | ||
Error.call(this); | ||
Error.captureStackTrace(this, this.constructor); | ||
}; | ||
ValidationAttributeError.prototype = new Error(); | ||
ValidationAttributeError.prototype.constructor = ValidationAttributeError; | ||
return ValidationAttributeError; | ||
})(), | ||
/** | ||
* @class <{Error}> | ||
@@ -631,3 +634,3 @@ * @prop {Array<{Attribute}>} attributes | ||
}; | ||
ValidationError.prototype = new Error(); | ||
ValidationError.prototype = Object.create(Error.prototype); | ||
ValidationError.prototype.constructor = ValidationError; | ||
@@ -634,0 +637,0 @@ return ValidationError; |
var Attribute = require('../attribute'), | ||
Collection = require('../collection'), | ||
Vow = require('vow'), | ||
CollectionAttribute; | ||
@@ -25,2 +26,29 @@ | ||
validate: function () { | ||
return Vow.allResolved(this.value.map(function (model) { | ||
return model.validate(); | ||
})).then(function (validationPromises) { | ||
var modelsErrors = [], | ||
error; | ||
validationPromises.forEach(function (promise, index) { | ||
var error; | ||
if (promise.isRejected()) { | ||
error = promise.valueOf(); | ||
error.index = index; | ||
modelsErrors.push(error); | ||
} | ||
}); | ||
if (modelsErrors.length) { | ||
error = new CollectionAttribute.ValidationError(); | ||
error.modelsErrors = modelsErrors; | ||
return Vow.reject(error); | ||
} else { | ||
return true; | ||
} | ||
}.bind(this)); | ||
}, | ||
__constructor: function () { | ||
@@ -126,4 +154,19 @@ this.__base.apply(this, arguments); | ||
}, { | ||
ValidationError: (function () { | ||
/** | ||
* @param {String} message | ||
*/ | ||
var ValidationError = function (message) { | ||
Attribute.ValidationError.call(this, message); | ||
this.name = 'CollectionAttributeValidationError'; | ||
}; | ||
ValidationError.prototype = Object.create(Attribute.ValidationError.prototype); | ||
ValidationError.constructor = ValidationError; | ||
return ValidationError; | ||
})() | ||
}); | ||
module.exports = CollectionAttribute; |
/** | ||
* Nested model attribute | ||
*/ | ||
var Attribute = require('../attribute'), ModelAttribute, ModelAttributeStatic; | ||
var Attribute = require('../attribute'), | ||
Vow = require('vow'), | ||
ModelAttribute, ModelAttributeStatic; | ||
@@ -47,3 +49,7 @@ /** | ||
validate: function () { | ||
return this.value.validate(); | ||
return this.value.validate().fail(function (modelValidationError) { | ||
var error = new ModelAttribute.ValidationError(); | ||
error.modelError = modelValidationError; | ||
return Vow.reject(error); | ||
}.bind(this)); | ||
}, | ||
@@ -169,2 +175,17 @@ | ||
}, { | ||
ValidationError: (function () { | ||
/** | ||
* @param {String} message | ||
*/ | ||
var ValidationError = function (message) { | ||
Attribute.ValidationError.call(this, message); | ||
this.name = 'ModelAttributeValidationError'; | ||
}; | ||
ValidationError.prototype = Object.create(Attribute.ValidationError.prototype); | ||
ValidationError.constructor = ValidationError; | ||
return ValidationError; | ||
})() | ||
}); | ||
@@ -186,2 +207,3 @@ | ||
ModelAttributeStatic.inherit = ModelAttribute.inherit.bind(ModelAttribute); | ||
ModelAttributeStatic.ValidationError = ModelAttribute.ValidationError; | ||
module.exports = ModelAttributeStatic; |
{ | ||
"description": "promise based, typed attributes, nested models and collections", | ||
"name": "promised-models2", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"repository": "git@github.com:bem-node/promised-models.git", | ||
@@ -6,0 +6,0 @@ "keywords": [ |
@@ -1,3 +0,3 @@ | ||
# Promised Models | ||
Promised Models 2 [](https://travis-ci.org/bem-node/promised-models) | ||
=== | ||
## Key features | ||
@@ -12,3 +12,3 @@ | ||
$npm install --save promised-models | ||
$npm install --save promised-models2 | ||
@@ -351,5 +351,3 @@ ## Usage | ||
}).then(function () { | ||
return true; //valid | ||
}, function () { | ||
return false; //invalid | ||
Vow.reject('Value is Invalid!'); | ||
}); | ||
@@ -364,3 +362,6 @@ } | ||
if (err instanceof Model.ValidationError) { | ||
console.log('Invalid attributes:' + err.attributes.join()); | ||
console.log('Invalid attributes:'); | ||
err.attributes.forEach(function (attrErr) { | ||
console.log('Attribute "' + attrErr.attribute.name + '" error:', attrError); | ||
}); | ||
} else { | ||
@@ -372,2 +373,30 @@ return err; | ||
Fulfilled attribute validation promise means that attribute is valid, otherwise it's not. `Model.ValidationError#attributes` is array of attributes errors (`Attribute.ValidationError`). | ||
**Note:** For `Model` and `Collection` attributes `validation` method is already defined. It validates nested entity and if it's not returns promise rejected with specific error contains nested errors. | ||
If `validate` returns promise rejected with `String` this string will be used as message for `Attribute.ValidationError`. If with something else (besides `Boolean`) - rejected value will be available in `Attribute.ValidationError#data`. | ||
If attribute can be validated synchronously, you can define `getValidationError` method. If it returns non-falsy value, validation promise will be rejected with returned value. | ||
```js | ||
var FashionModel = Model.inherit({ | ||
attributes: { | ||
name: Model.attributeTypes.String.inherit({ | ||
getValidationError: function () { | ||
if (this.get() !== 'validValue') { | ||
return 'Value is Invalid!'; | ||
} | ||
} | ||
}) | ||
} | ||
}), | ||
model = new FashionModel(); | ||
model.validate().fail(function (err) { | ||
console.log(err.attributes[0]); | ||
}); | ||
``` | ||
#### ready `model.ready()` | ||
@@ -374,0 +403,0 @@ |
@@ -5,2 +5,4 @@ /** | ||
var Models = require('../../lib/model'), | ||
Attribute = require('../../lib/attribute'), | ||
Vow = require('vow'), | ||
fulfill = require('../../lib/fulfill'); | ||
@@ -27,6 +29,20 @@ | ||
default: 'validValue', | ||
validate: function () { | ||
return this.value === 'validValue' ? true : new Models.ValidationAttributeError(this, 'error'); | ||
getValidationError: function () { | ||
return this.value !== 'validValue'; | ||
} | ||
}), | ||
withSyncValidationMessage: Models.attributeTypes.String.inherit({ | ||
type: 'string', | ||
default: 'validValue', | ||
getValidationError: function () { | ||
return this.value !== 'validValue' && 'invalid'; | ||
} | ||
}), | ||
withSyncValidationData: Models.attributeTypes.String.inherit({ | ||
type: 'string', | ||
default: 'validValue', | ||
getValidationError: function () { | ||
return this.value !== 'validValue' && {data: 'data'}; | ||
} | ||
}), | ||
withAsyncValidation: Models.attributeTypes.String.inherit({ | ||
@@ -36,6 +52,7 @@ type: 'string', | ||
validate: function () { | ||
var attribute = this; | ||
return fulfill().delay(0).then(function () { | ||
return attribute.value === 'validValue' ? true : new Models.ValidationAttributeError(this, 'error'); | ||
}); | ||
if (this.value !== 'validValue') { | ||
return Vow.reject('Invalid'); | ||
} | ||
}.bind(this)); | ||
} | ||
@@ -42,0 +59,0 @@ }) |
@@ -5,2 +5,3 @@ /** | ||
var Model = require('../../lib/model'), | ||
Attribute = require('../../lib/attribute'), | ||
NestedModel = Model.inherit({ | ||
@@ -11,4 +12,4 @@ attributes: { | ||
invalid: Model.attributeTypes.Number.inherit({ | ||
validate: function () { | ||
return Boolean(this.value); | ||
getValidationError: function () { | ||
return !this.value; | ||
} | ||
@@ -24,2 +25,5 @@ }) | ||
}), | ||
nestedCollection: Model.attributeTypes.Collection.inherit({ | ||
modelType: NestedModel | ||
}), | ||
nestedAsync: Model.attributeTypes.Model(require('./with-calculations')), | ||
@@ -26,0 +30,0 @@ collection: Model.attributeTypes.ModelsList(require('./with-calculations')), |
var expect = require('chai').expect, | ||
Attribute = require('../lib/attribute'), | ||
ModelAttribute = require('../lib/types/model'), | ||
CollectionAttribute = require('../lib/types/collection'), | ||
Vow = require('vow'); | ||
@@ -19,3 +22,3 @@ | ||
return model.validate().always(function (p) { | ||
if (p.isFullfiled) { | ||
if (p.isFulfilled()) { | ||
return Vow.reject(); | ||
@@ -30,4 +33,5 @@ } else { | ||
model.set('withAsyncValidation', 'notValid'); | ||
return model.validate(); | ||
}).always(function (p) { | ||
if (p.isFullfiled) { | ||
if (p.isFulfilled()) { | ||
return Vow.reject(); | ||
@@ -46,3 +50,3 @@ } else { | ||
var error; | ||
if (p.isFullfiled) { | ||
if (p.isFulfilled()) { | ||
return Vow.reject(); | ||
@@ -56,7 +60,100 @@ } else { | ||
expect(error).to.have.deep.property('attributes[1].name'); | ||
expect(error.attributes[0]).to.be.instanceOf(ModelClass.ValidationAttributeError); | ||
expect(error.attributes[0].type).to.be.equal('error'); | ||
expect(error.attributes[0]).to.be.instanceOf(Attribute.ValidationError); | ||
} | ||
}); | ||
}); | ||
it('should set error message if getValidationError returns string', function () { | ||
model.set({ | ||
withSyncValidationMessage: 'notValid' | ||
}); | ||
return model.validate().always(function (p) { | ||
var error; | ||
if (p.isFulfilled()) { | ||
return Vow.reject(); | ||
} else { | ||
error = p.valueOf(); | ||
expect(error.attributes[0]).to.be.instanceOf(Attribute.ValidationError); | ||
expect(error.attributes[0].message).to.be.equal('invalid'); | ||
} | ||
}); | ||
}); | ||
it('should set data to error if getValidationError returns any besides string or boolean', function () { | ||
model.set({ | ||
withSyncValidationData: 'notValid' | ||
}); | ||
return model.validate().always(function (p) { | ||
var error; | ||
if (p.isFulfilled()) { | ||
return Vow.reject(); | ||
} else { | ||
error = p.valueOf(); | ||
expect(error.attributes[0]).to.be.instanceOf(Attribute.ValidationError); | ||
expect(error.attributes[0].data).to.be.deep.equal({data: 'data'}); | ||
} | ||
}); | ||
}); | ||
describe('with nested entities', function () { | ||
var model, | ||
ModelClass = require('./models/with-nested'); | ||
beforeEach(function () { | ||
model = new ModelClass({ | ||
a: 'a1', | ||
nested: { | ||
invalid: true | ||
} | ||
}); | ||
}); | ||
it('should report invalid nested model attribute with specific error', function () { | ||
model.get('nested').set('invalid', false); | ||
return model.validate().always(function (p) { | ||
var error; | ||
if (p.isFulfilled()) { | ||
return Vow.reject(); | ||
} else { | ||
error = p.valueOf(); | ||
expect(error).to.be.instanceOf(Error); | ||
expect(error).to.be.instanceOf(ModelClass.ValidationError); | ||
expect(error.attributes[0]).to.be.instanceOf(ModelAttribute.ValidationError); | ||
expect(error.attributes[0].modelError).to.be.instanceOf(ModelClass.ValidationError); | ||
} | ||
}); | ||
}); | ||
it('should report invalid nested collection attribute with specific error', function () { | ||
model.get('nestedCollection').set([ | ||
{invalid: false}, | ||
{invalid: true}, | ||
{invalid: false} | ||
]); | ||
return model.validate().always(function (p) { | ||
var error; | ||
if (p.isFullfiled) { | ||
return Vow.reject(); | ||
} else { | ||
error = p.valueOf(); | ||
expect(error).to.be.instanceOf(Error); | ||
expect(error).to.be.instanceOf(ModelClass.ValidationError); | ||
expect(error.attributes[0]).to.be.instanceOf(CollectionAttribute.ValidationError); | ||
expect(error.attributes[0].modelsErrors).to.be.an('array'); | ||
expect(error.attributes[0].modelsErrors.length).to.be.equal(2); | ||
expect(error.attributes[0].modelsErrors[0]).to.be.instanceOf(ModelClass.ValidationError); | ||
expect(error.attributes[0].modelsErrors[0].index).to.be.equal(0); | ||
expect(error.attributes[0].modelsErrors[1]).to.be.instanceOf(ModelClass.ValidationError); | ||
expect(error.attributes[0].modelsErrors[1].index).to.be.equal(2); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
169038
4320
707