Skip to content

Commit

Permalink
Validations
Browse files Browse the repository at this point in the history
  • Loading branch information
1602 committed Oct 10, 2011
1 parent b2ea9c6 commit 54ddb55
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 16 deletions.
6 changes: 3 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -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;
40 changes: 30 additions & 10 deletions lib/abstract-class.js
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions lib/jutil.js
Original file line number Diff line number Diff line change
@@ -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];
});
};

11 changes: 10 additions & 1 deletion lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
});
}

192 changes: 192 additions & 0 deletions lib/validatable.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": ""
Expand Down
Loading

0 comments on commit 54ddb55

Please sign in to comment.