Permalink
Browse files

Validations

  • Loading branch information...
1602 committed Oct 10, 2011
1 parent b2ea9c6 commit 54ddb55c498bc7c9a679f9f37bc775704d167a41
Showing with 418 additions and 16 deletions.
  1. +3 −3 index.js
  2. +30 −10 lib/abstract-class.js
  3. +9 −0 lib/jutil.js
  4. +10 −1 lib/schema.js
  5. +192 −0 lib/validatable.js
  6. +2 −2 package.json
  7. +172 −0 test/validations_test.coffee
View
@@ -1,3 +1,3 @@
-exports.Schema = require('./lib/schema');
-exports.AbstractClass = require('./lib/abstract-class');
-exports.Validatable = require('./lib/validatable');
+exports.Schema = require('./lib/schema').Schema;
+exports.AbstractClass = require('./lib/abstract-class').AbstractClass;
+exports.Validatable = require('./lib/validatable').Validatable;
View
@@ -1,3 +1,14 @@
+/**
+ * Module deps
+ */
+var Validatable = require('./validatable').Validatable;
+var util = require('util');
+var jutil = require('./jutil');
+
+exports.AbstractClass = AbstractClass;
+
+jutil.inherits(AbstractClass, Validatable);
+
/**
* Abstract class constructor
*/
@@ -73,13 +84,20 @@ AbstractClass.create = function (data) {
}
var obj = null;
+ // if we come from save
if (data instanceof AbstractClass && !data.id) {
obj = data;
data = obj.toObject();
+ } else {
+ obj = new this(data);
+
+ // validation required
+ if (!obj.isValid()) {
+ return callback(new Error('Validation error'), obj);
+ }
}
this.schema.adapter.create(modelName, data, function (err, id) {
- obj = obj || new this(data);
if (id) {
defineReadonlyProp(obj, 'id', id);
this.cache[id] = obj;
@@ -195,6 +213,10 @@ AbstractClass.prototype.save = function (options, callback) {
}
};
+AbstractClass.prototype.isNewRecord = function () {
+ return !this.id;
+};
+
AbstractClass.prototype._adapter = function () {
return this.constructor.schema.adapter;
};
@@ -227,6 +249,13 @@ AbstractClass.prototype.updateAttribute = function (name, value, cb) {
AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
var model = this.constructor.modelName;
+ Object.keys(data).forEach(function (key) {
+ this[key] = data[key];
+ }.bind(this));
+ if (!this.isValid()) {
+ var err = new Error('Validation error');
+ return cb && cb(err);
+ }
this._adapter().updateAttributes(model, this.id, data, function (err) {
if (!err) {
Object.keys(data).forEach(function (key) {
@@ -333,15 +362,6 @@ function merge(base, update) {
return base;
}
-function hiddenProperty(where, property, value) {
- Object.defineProperty(where, property, {
- writable: false,
- enumerable: false,
- configurable: false,
- value: value
- });
-}
-
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
View
@@ -0,0 +1,9 @@
+exports.inherits = function (newClass, baseClass) {
+ Object.keys(baseClass).forEach(function (classMethod) {
+ newClass[classMethod] = baseClass[classMethod];
+ });
+ Object.keys(baseClass.prototype).forEach(function (instanceMethod) {
+ newClass.prototype[instanceMethod] = baseClass.prototype[instanceMethod];
+ });
+};
+
View
@@ -102,7 +102,7 @@ Schema.prototype.define = function defineClass(className, properties, settings)
// setup inheritance
newClass.__proto__ = AbstractClass;
- require('util').inherits(newClass, AbstractClass);
+ util.inherits(newClass, AbstractClass);
// store class in model pool
this.models[className] = newClass;
@@ -150,3 +150,12 @@ Schema.prototype.defineForeignKey = function defineForeignKey(className, key) {
};
+function hiddenProperty(where, property, value) {
+ Object.defineProperty(where, property, {
+ writable: false,
+ enumerable: false,
+ configurable: false,
+ value: value
+ });
+}
+
View
@@ -0,0 +1,192 @@
+exports.Validatable = Validatable;
+
+function Validatable() {
+ // validatable class
+};
+
+Validatable.validatesPresenceOf = getConfigurator('presence');
+Validatable.validatesLengthOf = getConfigurator('length');
+Validatable.validatesNumericalityOf = getConfigurator('numericality');
+Validatable.validatesInclusionOf = getConfigurator('inclusion');
+Validatable.validatesExclusionOf = getConfigurator('exclusion');
+
+function getConfigurator(name) {
+ return function () {
+ configure(this, name, arguments);
+ };
+}
+
+Validatable.prototype.isValid = function () {
+ var valid = true, inst = this;
+ if (!this.constructor._validations) {
+ Object.defineProperty(this, 'errors', {
+ enumerable: false,
+ configurable: true,
+ value: false
+ });
+ return valid;
+ }
+ Object.defineProperty(this, 'errors', {
+ enumerable: false,
+ configurable: true,
+ value: new Errors
+ });
+ this.constructor._validations.forEach(function (v) {
+ if (validationFailed(inst, v)) {
+ valid = false;
+ }
+ });
+ if (valid) {
+ Object.defineProperty(this, 'errors', {
+ enumerable: false,
+ configurable: true,
+ value: false
+ });
+ }
+ return valid;
+};
+
+function validationFailed(inst, v) {
+ var attr = v[0];
+ var conf = v[1];
+ // here we should check skip validation conditions (if, unless)
+ // that can be specified in conf
+ var fail = false;
+ validators[conf.validation].call(inst, attr, conf, function onerror(kind) {
+ var message;
+ if (conf.message) {
+ message = conf.message;
+ }
+ if (!message && defaultMessages[conf.validation]) {
+ message = defaultMessages[conf.validation];
+ }
+ if (!message) {
+ message = 'is invalid';
+ }
+ if (kind) {
+ if (message[kind]) {
+ // get deeper
+ message = message[kind];
+ } else if (defaultMessages.common[kind]) {
+ message = defaultMessages.common[kind];
+ }
+ }
+ inst.errors.add(attr, message);
+ fail = true;
+ });
+ return fail;
+}
+
+var defaultMessages = {
+ presence: 'can\'t be blank',
+ length: {
+ min: 'too short',
+ max: 'too long',
+ is: 'length is wrong'
+ },
+ common: {
+ blank: 'is blank',
+ 'null': 'is null'
+ },
+ numericality: {
+ 'int': 'is not an integer',
+ 'number': 'is not a number'
+ },
+ inclusion: 'is not included in the list',
+ exclusion: 'is reserved'
+};
+
+var validators = {
+ presence: function (attr, conf, err) {
+ if (blank(this[attr])) {
+ err();
+ }
+ },
+ length: function (attr, conf, err) {
+ var isNull = this[attr] === null || !(attr in this);
+ if (isNull) {
+ if (conf.allowNull) {
+ return true;
+ } else {
+ return err('null');
+ }
+ } else {
+ if (blank(this[attr])) {
+ if (conf.allowBlank) {
+ return true;
+ } else {
+ return err('blank');
+ }
+ }
+ }
+ var len = this[attr].length;
+ if (conf.min && len < conf.min) {
+ err('min');
+ }
+ if (conf.max && len > conf.max) {
+ err('max');
+ }
+ if (conf.is && len !== conf.is) {
+ err('is');
+ }
+ },
+ numericality: function (attr, conf, err) {
+ if (typeof this[attr] !== 'number') {
+ return err('number');
+ }
+ if (conf.int && this[attr] !== Math.round(this[attr])) {
+ return err('int');
+ }
+ },
+ inclusion: function (attr, conf, err) {
+ if (!~conf.in.indexOf(this[attr])) {
+ err()
+ }
+ },
+ exclusion: function (attr, conf, err) {
+ if (~conf.in.indexOf(this[attr])) {
+ err()
+ }
+ }
+};
+
+function blank(v) {
+ if (typeof v === 'undefined') return true;
+ if (v instanceof Array && v.length === 0) return true;
+ if (v === null) return true;
+ if (typeof v == 'string' && v === '') return true;
+ return false;
+}
+
+function configure(class, validation, args) {
+ if (!class._validations) {
+ Object.defineProperty(class, '_validations', {
+ writable: true,
+ configurable: true,
+ enumerable: false,
+ value: []
+ });
+ }
+ args = [].slice.call(args);
+ var conf;
+ if (typeof args[args.length - 1] === 'object') {
+ conf = args.pop();
+ } else {
+ conf = {};
+ }
+ conf.validation = validation;
+ args.forEach(function (attr) {
+ class._validations.push([attr, conf]);
+ });
+}
+
+function Errors() {
+}
+
+Errors.prototype.add = function (field, message) {
+ if (!this[field]) {
+ this[field] = [message];
+ } else {
+ this[field].push(message);
+ }
+};
View
@@ -1,7 +1,7 @@
{
"author": "Anatoliy Chakkaev",
- "name": "yadm",
- "description": "Yet another data mapper",
+ "name": "jugglingdb",
+ "description": "ORM for every database: redis, mysql, neo4j, mongodb",
"version": "0.0.1",
"repository": {
"url": ""
Oops, something went wrong.

0 comments on commit 54ddb55

Please sign in to comment.