This commit is contained in:
Anatoliy Chakkaev 2012-03-27 18:22:24 +04:00
parent 30d0818eed
commit 6ee7de0716
8 changed files with 800 additions and 155 deletions

View File

@ -1,11 +1,18 @@
var fs = require('fs');
var path = require('path');
exports.Schema = require('./lib/schema').Schema;
exports.AbstractClass = require('./lib/abstract-class').AbstractClass;
exports.Validatable = require('./lib/validatable').Validatable;
exports.init = function () {
if (!global.railway) return;
railway.orm = exports;
require('./lib/railway');
};
try {
if (process.versions.node < '0.6' || true) {
if (process.versions.node < '0.6') {
exports.version = JSON.parse(fs.readFileSync(__dirname + '/package.json')).version;
} else {
exports.version = require('../package').version;

View File

@ -1,10 +1,10 @@
/**
* Module deps
* Module dependencies
*/
var Validatable = require('./validatable').Validatable;
var Hookable = require('./hookable').Hookable;
var util = require('util');
var jutil = require('./jutil');
var Validatable = require('./validatable').Validatable;
var Hookable = require('./hookable').Hookable;
var DEFAULT_CACHE_LIMIT = 1000;
exports.AbstractClass = AbstractClass;
@ -13,7 +13,15 @@ jutil.inherits(AbstractClass, Validatable);
jutil.inherits(AbstractClass, Hookable);
/**
* Abstract class constructor
* Abstract class - base class for all persist objects
* provides **common API** to access any database adapter.
* This class describes only abstract behavior layer, refer to `lib/adapters/*.js`
* to learn more about specific adapter implementations
*
* `AbstractClass` mixes `Validatable` and `Hookable` classes methods
*
* @constructor
* @param {Object} data - initial object data
*/
function AbstractClass(data) {
var self = this;
@ -99,6 +107,10 @@ function AbstractClass(data) {
AbstractClass.setter = {};
AbstractClass.getter = {};
/**
* @param {String} prop - property name
* @param {Object} params - various property configuration
*/
AbstractClass.defineProperty = function (prop, params) {
this.schema.defineProperty(this.modelName, prop, params);
};
@ -113,8 +125,14 @@ AbstractClass.prototype.whatTypeName = function (propName) {
};
/**
* Create new instance of Model class, saved in database
*
* @param data [optional]
* @param callback(err, obj)
* callback called with arguments:
*
* - err (null or Error)
* - instance (null or Model)
*/
AbstractClass.create = function (data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -178,6 +196,9 @@ function stillConnecting(schema, obj, args) {
return true;
};
/**
* Update or insert
*/
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -204,6 +225,12 @@ AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, call
}
};
/**
* Check whether object exitst in database
*
* @param {id} id - identifier of object (primary key value)
* @param {Function} cb - callbacl called with (err, exists: Bool)
*/
AbstractClass.exists = function exists(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -214,6 +241,12 @@ AbstractClass.exists = function exists(id, cb) {
}
};
/**
* Find object by id
*
* @param {id} id - primary key value
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.find = function find(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -236,9 +269,20 @@ AbstractClass.find = function find(id, cb) {
};
/**
* Query collection of objects
* @param params {where: {}, order: '', limit: 1, offset: 0,...}
* @param cb (err, array of AbstractClass)
* Find all instances of Model, matched by query
* make sure you have marked as `index: true` fields for filter or sort
*
* @param {Object} params (optional)
*
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
* - order: String
* - limit: Number
* - skip: Number
*
* @param {Function} callback (required) called with arguments:
*
* - err (null or Error)
* - Array of instances
*/
AbstractClass.all = function all(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -271,6 +315,12 @@ AbstractClass.all = function all(params, cb) {
});
};
/**
* Find one record, same as `all`, limited by 1 and return object, not collection
*
* @param {Object} params - search conditions
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.findOne = function findOne(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -293,6 +343,10 @@ function substractDirtyAttributes(object, data) {
});
}
/**
* Destroy all records
* @param {Function} cb - callback called with (err)
*/
AbstractClass.destroyAll = function destroyAll(cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -302,6 +356,12 @@ AbstractClass.destroyAll = function destroyAll(cb) {
}.bind(this));
};
/**
* Return count of matched records
*
* @param {Object} where - search conditions (optional)
* @param {Function} cb - callback, called with (err, count)
*/
AbstractClass.count = function (where, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@ -312,6 +372,11 @@ AbstractClass.count = function (where, cb) {
this.schema.adapter.count(this.modelName, cb, where);
};
/**
* Return string representation of class
*
* @override default toString method
*/
AbstractClass.toString = function () {
return '[Model ' + this.modelName + ']';
}
@ -393,14 +458,22 @@ AbstractClass.prototype.isNewRecord = function () {
return !this.id;
};
/**
* Return adapter of current record
* @private
*/
AbstractClass.prototype._adapter = function () {
return this.constructor.schema.adapter;
};
AbstractClass.prototype.propertyChanged = function (name) {
return this[name + '_was'] !== this['_' + name];
};
/**
* Convert instance to Object
*
* @param {Boolean} onlySchema - restrict properties to schema only, default false
* when onlySchema == true, only properties defined in schema returned,
* otherwise all enumerable properties returned
* @returns {Object} - canonical object representation (no getters and setters)
*/
AbstractClass.prototype.toObject = function (onlySchema) {
var data = {};
var ds = this.constructor.schema.definitions[this.constructor.modelName];
@ -412,6 +485,11 @@ AbstractClass.prototype.toObject = function (onlySchema) {
return data;
};
/**
* Delete object from persistence
*
* @triggers `destroy` hook (async) before and after destroying object
*/
AbstractClass.prototype.destroy = function (cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
@ -425,14 +503,30 @@ AbstractClass.prototype.destroy = function (cb) {
});
};
AbstractClass.prototype.updateAttribute = function (name, value, cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
/**
* Update single attribute
*
* equals to `updateAttributes({name: value}, cb)
*
* @param {String} name - name of property
* @param {Mixed} value - value of property
* @param {Function} callback - callback called with (err, instance)
*/
AbstractClass.prototype.updateAttribute = function updateAttribute(name, value, callback) {
data = {};
data[name] = value;
this.updateAttributes(data, cb);
this.updateAttributes(data, callback);
};
/**
* Update set of attributes
*
* this method performs validation before updating
*
* @trigger `validation`, `save` and `update` hooks
* @param {Object} data - data to update
* @param {Function} callback - callback called with (err, instance)
*/
AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
@ -490,23 +584,34 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
/**
* Checks is property changed based on current property and initial value
* @param {attr} String - property name
*
* @param {String} attr - property name
* @return Boolean
*/
AbstractClass.prototype.propertyChanged = function (attr) {
AbstractClass.prototype.propertyChanged = function propertyChanged(attr) {
return this['_' + attr] !== this[attr + '_was'];
};
AbstractClass.prototype.reload = function (cb) {
/**
* Reload object from persistence
*
* @requires `id` member of `object` to be able to call `find`
* @param {Function} callback - called with (err, instance) arguments
*/
AbstractClass.prototype.reload = function reload(callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
var obj = getCached(this.constructor, this.id);
if (obj) {
obj.reset();
}
this.constructor.find(this.id, cb);
if (obj) obj.reset();
this.constructor.find(this.id, callback);
};
/**
* Reset dirty attributes
*
* this method does not perform any database operation it just reset object to it's
* initial state
*/
AbstractClass.prototype.reset = function () {
var obj = this;
Object.keys(obj).forEach(function (k) {
@ -519,8 +624,14 @@ AbstractClass.prototype.reset = function () {
});
};
// relations
AbstractClass.hasMany = function (anotherClass, params) {
/**
* Declare hasMany relation
*
* @param {Class} anotherClass - class to has many
* @param {Object} params - configuration {as:, foreignKey:}
* @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
*/
AbstractClass.hasMany = function hasMany(anotherClass, params) {
var methodName = params.as; // or pluralize(anotherClass.modelName)
var fk = params.foreignKey;
// each instance of this class should have method named
@ -562,6 +673,12 @@ AbstractClass.hasMany = function (anotherClass, params) {
};
/**
* Declare belongsTo relation
*
* @param {Class} anotherClass - class to belong
* @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'}
*/
AbstractClass.belongsTo = function (anotherClass, params) {
var methodName = params.as;
var fk = params.foreignKey;
@ -596,6 +713,10 @@ AbstractClass.belongsTo = function (anotherClass, params) {
};
/**
* Define scope
* TODO: describe behavior and usage examples
*/
AbstractClass.scope = function (name, params) {
defineScope(this, this, name, params);
};
@ -689,14 +810,23 @@ function defineScope(cls, targetClass, name, params, methods) {
}
}
// helper methods
//
/**
* Check whether `s` is not undefined
* @param {Mixed} s
* @return {Boolean} s is undefined
*/
function isdef(s) {
var undef;
return s !== undef;
}
/**
* Merge `base` and `update` params
* @param {Object} base - base object (updating this object)
* @param {Object} update - object with new data to update base
* @returns {Object} `base`
*/
function merge(base, update) {
base = base || {};
if (update) {
@ -707,6 +837,13 @@ function merge(base, update) {
return base;
}
/**
* Define readonly property on object
*
* @param {Object} obj
* @param {String} key
* @param {Mixed} value
*/
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
@ -716,11 +853,17 @@ function defineReadonlyProp(obj, key, value) {
});
}
/**
* Add object to cache
*/
function addToCache(constr, obj) {
touchCache(constr, obj.id);
constr.cache[obj.id] = obj;
}
/**
* Renew object position in LRU cache index
*/
function touchCache(constr, id) {
var cacheLimit = constr.CACHE_LIMIT || DEFAULT_CACHE_LIMIT;
@ -734,16 +877,32 @@ function touchCache(constr, id) {
}
}
/**
* Retrieve cached object
*/
function getCached(constr, id) {
if (id) touchCache(constr, id);
return id && constr.cache[id];
}
/**
* Clear cache (fully)
*
* removes both cache and LRU index
*
* @param {Class} constr - class constructor
*/
function clearCache(constr) {
constr.cache = {};
constr.mru = [];
}
/**
* Remove object from cache
*
* @param {Class} constr
* @param {id} id
*/
function removeFromCache(constr, id) {
var ind = constr.mru.indexOf(id);
if (!~ind) constr.mru.splice(ind, 1);

View File

@ -35,6 +35,9 @@ exports.initialize = function initializeSchema(schema, callback) {
});
};
/**
* MySQL adapter
*/
function MySQL(client) {
this._models = {};
this.client = client;

View File

@ -54,7 +54,7 @@ BridgeToRedis.prototype.save = function (model, data, callback) {
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback) {
var i = this.indexes[model];
var schedule = [];
var schedule = [['sadd', 's:' + model, id]];
Object.keys(data).forEach(function (key) {
if (i[key]) {
schedule.push([

199
lib/railway.js Normal file
View File

@ -0,0 +1,199 @@
var fs = require('fs');
var path = require('path');
var Schema = railway.orm.Schema;
railway.orm._schemas = [];
try {
var config = JSON.parse(fs.readFileSync(app.root + '/config/database.json', 'utf-8'))[app.set('env')];
} catch (e) {
console.log('Could not parse config/database.json');
throw e;
}
var schema = new Schema(config && config.driver || 'memory', config);
schema.log = log;
railway.orm._schemas.push(schema);
context = prepareContext(schema);
// run schema first
var schemaFile = app.root + '/db/schema.';
if (path.existsSync(schemaFile + 'js')) {
schemaFile += 'js';
} else {
schemaFile += 'coffee';
}
runCode(schemaFile, context);
// and freeze schemas
railway.orm._schemas.forEach(function (schema) {
schema.freeze();
});
function log(str, startTime) {
var $ = utils.stylize.$;
var m = Date.now() - startTime;
utils.debug(str + $(' [' + (m < 10 ? m : $(m).red) + ' ms]').bold);
app.emit('app-event', {
type: 'query',
param: str,
time: m
});
}
function runCode(filename, context) {
var isCoffee = filename.match(/coffee$/);
context = context || {};
var dirname = path.dirname(filename);
// extend context
context.require = context.require || function (apath) {
var isRelative = apath.match(/^\.\.?\//);
return require(isRelative ? path.resolve(dirname, apath) : apath);
};
context.app = app;
context.railway = railway;
context.console = console;
context.setTimeout = setTimeout;
context.setInterval = setInterval;
context.clearTimeout = clearTimeout;
context.clearInterval = clearInterval;
context.__filename = filename;
context.__dirname = dirname;
context.process = process;
context.t = context.t || t;
context.Buffer = Buffer;
var code = path.existsSync(filename) && require('fs').readFileSync(filename);
if (!code) return;
if (isCoffee) {
try {
var cs = require('coffee-script');
} catch (e) {
throw new Error('Please install coffee-script npm package: `npm install coffee-script`');
}
try {
code = require('coffee-script').compile(code);
} catch (e) {
console.log('Error in coffee code compilation in file ' + filename);
throw e;
}
}
try {
var m = require('vm').createScript(code.toString('utf8'), filename);
m.runInNewContext(context);
} catch (e) {
console.log('Error while executing ' + filename);
throw e;
}
}
function prepareContext(defSchema, done) {
var ctx = {app: app},
models = {},
settings = {},
cname,
schema,
wait = connected = 0,
nonJugglingSchema = false;
done = done || function () {};
/**
* Multiple schemas support
* example:
* schema('redis', {url:'...'}, function () {
* describe models using redis connection
* ...
* });
* schema(function () {
* describe models stored in memory
* ...
* });
*/
ctx.schema = function () {
var name = argument('string');
var opts = argument('object') || {};
var def = argument('function') || function () {};
schema = new Schema(name || opts.driver || 'memory', opts);
railway.orm._schemas.push(schema);
wait += 1;
ctx.gotSchema = true;
schema.on('log', log);
schema.on('connected', function () {
if (wait === ++connected) done();
});
def();
schema = false;
};
/**
* Use custom schema driver
*/
ctx.customSchema = function () {
var def = argument('function') || function () {};
nonJugglingSchema = true;
def();
Object.keys(ctx.exports).forEach(function (m) {
ctx.define(m, ctx.exports[m]);
});
nonJugglingSchema = false;
};
ctx.exports = {};
ctx.module = { exports: ctx.exports };
/**
* Define a class in current schema
*/
ctx.describe = ctx.define = function (className, callback) {
var m;
cname = className;
models[cname] = {};
settings[cname] = {};
if (nonJugglingSchema) {
m = callback;
} else {
callback && callback();
m = (schema || defSchema).define(className, models[cname], settings[cname]);
}
return global[cname] = app.models[cname] = ctx[cname] = m;
};
/**
* Define a property in current class
*/
ctx.property = function (name, type, params) {
if (!params) params = {};
if (typeof type !== 'function' && typeof type === 'object') {
params = type;
type = String;
}
params.type = type || String;
models[cname][name] = params;
};
/**
* Set custom table name for current class
* @param name - name of table
*/
ctx.setTableName = function (name) {
if (cname) settings[cname].table = name;
};
ctx.Text = Schema.Text;
return ctx;
function argument(type) {
var r;
[].forEach.call(arguments.callee.caller.arguments, function (a) {
if (!r && typeof a === type) r = a;
});
return r;
}
}

View File

@ -17,10 +17,30 @@ exports.Schema = Schema;
var slice = Array.prototype.slice;
/**
* Shema - classes factory
* Schema - adapter-specific classes factory.
*
* All classes in single schema shares same adapter type and
* one database connection
*
* @param name - type of schema adapter (mysql, mongoose, sequelize, redis)
* @param settings - any database-specific settings which we need to
* establish connection (of course it depends on specific adapter)
*
* - host
* - port
* - username
* - password
* - database
* - debug {Boolean} = false
*
* @example Schema creation, waiting for connection callback
* ```
* var schema = new Schema('mysql', { database: 'myapp_test' });
* schema.define(...);
* schema.on('connected', function () {
* // work with database
* });
* ```
*/
function Schema(name, settings) {
var schema = this;
@ -73,65 +93,40 @@ function Schema(name, settings) {
util.inherits(Schema, require('events').EventEmitter);
function Text() {
}
function Text() {}
Schema.Text = Text;
Schema.prototype.defineProperty = function (model, prop, params) {
this.definitions[model].properties[prop] = params;
if (this.adapter.defineProperty) {
this.adapter.defineProperty(model, prop, params);
}
};
Schema.prototype.automigrate = function (cb) {
this.freeze();
if (this.adapter.automigrate) {
this.adapter.automigrate(cb);
} else if (cb) {
cb();
}
};
Schema.prototype.autoupdate = function (cb) {
this.freeze();
if (this.adapter.autoupdate) {
this.adapter.autoupdate(cb);
} else if (cb) {
cb();
}
};
/**
* Check whether migrations needed
*/
Schema.prototype.isActual = function (cb) {
this.freeze();
if (this.adapter.isActual) {
this.adapter.isActual(cb);
} else if (cb) {
cb(null, true);
}
};
Schema.prototype.log = function (sql, t) {
this.emit('log', sql, t);
};
Schema.prototype.freeze = function freeze() {
if (this.adapter.freezeSchema) {
this.adapter.freezeSchema();
}
}
/**
* Define class
* @param className
* @param properties - hash of class properties in format
* {property: Type, property2: Type2, ...}
* or
* {property: {type: Type}, property2: {type: Type2}, ...}
* @param settings - other configuration of class
*
* @param {String} className
* @param {Object} properties - hash of class properties in format
* `{property: Type, property2: Type2, ...}`
* or
* `{property: {type: Type}, property2: {type: Type2}, ...}`
* @param {Object} settings - other configuration of class
* @return newly created class
*
* @example simple case
* ```
* var User = schema.defind('User', {
* email: String,
* password: String,
* birthDate: Date,
* activated: Boolean
* });
* ```
*
* @example more advanced case
* ```
* var User = schema.defind('User', {
* email: { type: String, limit: 150, index: true },
* password: { type: String, limit: 50 },
* birthDate: Date,
* registrationDate: {type: Date, default: function () { return new Date }},
* activated: { type: Boolean, default: false }
* });
* ```
*/
Schema.prototype.define = function defineClass(className, properties, settings) {
var schema = this;
@ -191,10 +186,94 @@ Schema.prototype.define = function defineClass(className, properties, settings)
};
/**
* Define single property named `prop` on `model`
*
* @param {String} model - name of model
* @param {String} prop - name of propery
* @param {Object} params - property settings
*/
Schema.prototype.defineProperty = function (model, prop, params) {
this.definitions[model].properties[prop] = params;
if (this.adapter.defineProperty) {
this.adapter.defineProperty(model, prop, params);
}
};
/**
* Drop each model table and re-create.
* This method make sense only for sql adapters.
*
* @warning All data will be lost! Use autoupdate if you need your data.
*/
Schema.prototype.automigrate = function (cb) {
this.freeze();
if (this.adapter.automigrate) {
this.adapter.automigrate(cb);
} else if (cb) {
cb();
}
};
/**
* Update existing database tables.
* This method make sense only for sql adapters.
*/
Schema.prototype.autoupdate = function (cb) {
this.freeze();
if (this.adapter.autoupdate) {
this.adapter.autoupdate(cb);
} else if (cb) {
cb();
}
};
/**
* Check whether migrations needed
* This method make sense only for sql adapters.
*/
Schema.prototype.isActual = function (cb) {
this.freeze();
if (this.adapter.isActual) {
this.adapter.isActual(cb);
} else if (cb) {
cb(null, true);
}
};
/**
* Log benchmarked message. Do not redefine this method, if you need to grab
* chema logs, use `schema.on('log', ...)` emitter event
*
* @private used by adapters
*/
Schema.prototype.log = function (sql, t) {
this.emit('log', sql, t);
};
/**
* Freeze schema. Behavior depends on adapter
*/
Schema.prototype.freeze = function freeze() {
if (this.adapter.freezeSchema) {
this.adapter.freezeSchema();
}
}
/**
* Return table name for specified `modelName`
* @param {String} modelName
*/
Schema.prototype.tableName = function (modelName) {
return this.definitions[modelName].settings.table = this.definitions[modelName].settings.table || modelName
};
/**
* Define foreign key
* @param {String} className
* @param {String} key - name of key field
*/
Schema.prototype.defineForeignKey = function defineForeignKey(className, key) {
// return if already defined
if (this.definitions[className].properties[key]) return;
@ -209,13 +288,18 @@ Schema.prototype.defineForeignKey = function defineForeignKey(className, key) {
}
};
/**
* Close database connection
*/
Schema.prototype.disconnect = function disconnect() {
if (typeof this.adapter.disconnect === 'function') {
this.adapter.disconnect();
}
};
/**
* Define hidden property
*/
function hiddenProperty(where, property, value) {
Object.defineProperty(where, property, {
writable: false,

View File

@ -1,6 +1,10 @@
module.exports = BaseSQL;
function BaseSQL() {}
/**
* Base SQL class
*/
function BaseSQL() {
}
BaseSQL.prototype.query = function () {
throw new Error('query method should be declared in adapter');

View File

@ -1,92 +1,255 @@
exports.Validatable = Validatable;
/**
* Validation encapsulated in this abstract class.
*
* Basically validation configurators is just class methods, which adds validations
* configs to AbstractClass._validations. Each of this validations run when
* `obj.isValid()` method called.
*
* Each configurator can accept n params (n-1 field names and one config). Config
* is {Object} depends on specific validation, but all of them has one common part:
* `message` member. It can be just string, when only one situation possible,
* e.g. `Post.validatesPresenceOf('title', { message: 'can not be blank' });`
*
* In more complicated cases it can be {Hash} of messages (for each case):
* `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
*/
function Validatable() {
// validatable class
};
/**
* Validate presence. This validation fails when validated field is blank.
*
* Default error message "can't be blank"
*
* @example `Post.validatesPresenceOf('title')`
* @example `Post.validatesPresenceOf('title', {message: 'Can not be blank'})`
* @sync
*
* @nocode
* @see helper/validatePresence
*/
Validatable.validatesPresenceOf = getConfigurator('presence');
/**
* Validate length. Three kinds of validations: min, max, is.
*
* Default error messages:
*
* - min: too short
* - max: too long
* - is: length is wrong
*
* @example `User.validatesLengthOf('password', {min: 7});`
* @example `User.validatesLengthOf('email', {max: 100});`
* @example `User.validatesLengthOf('state', {is: 2});`
* @example `User.validatesLengthOf('nick', {min: 3, max: 15});
* @sync
*
* @nocode
* @see helper/validateLength
*/
Validatable.validatesLengthOf = getConfigurator('length');
/**
* Validate numericality.
*
* @example `User.validatesNumericalityOf('age', { message: { number: '...' }});`
* @example `User.validatesNumericalityOf('age', {int: true, message: { int: '...' }});`
*
* Default error messages:
*
* - number: is not a number
* - int: is not an integer
*
* @sync
*
* @nocode
* @see helper/validateNumericality
*/
Validatable.validatesNumericalityOf = getConfigurator('numericality');
/**
* Validate inclusion in set
*
* @example `User.validatesInclusionOf('gender', {in: ['male', 'female']});`
*
* Default error message: is not included in the list
*
* @nocode
* @see helper/validateInclusion
*/
Validatable.validatesInclusionOf = getConfigurator('inclusion');
/**
* Validate exclusion
*
* @example `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});`
*
* Default error message: is reserved
*
* @nocode
* @see helper/validateExclusion
*/
Validatable.validatesExclusionOf = getConfigurator('exclusion');
/**
* Validate format
*
* Default error message: is invalid
*
* @nocode
* @see helper/validateFormat
*/
Validatable.validatesFormatOf = getConfigurator('format');
/**
* Validate using custom validator
*
* Default error message: is invalid
*
* @nocode
* @see helper/validateCustom
*/
Validatable.validate = getConfigurator('custom');
/**
* Validate using custom async validator
*
* Default error message: is invalid
*
* @async
* @nocode
* @see helper/validateCustom
*/
Validatable.validateAsync = getConfigurator('custom', {async: true});
/**
* Validate uniqueness
*
* Default error message: is not unique
*
* @async
* @nocode
* @see helper/validateUniqueness
*/
Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
// implementation of validators
var validators = {
presence: function (attr, conf, err) {
if (blank(this[attr])) {
err();
}
},
length: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
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 (nullCheck.call(this, attr, conf, err)) return;
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 (nullCheck.call(this, attr, conf, err)) return;
if (!~conf.in.indexOf(this[attr])) {
err()
}
},
exclusion: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
if (~conf.in.indexOf(this[attr])) {
err()
}
},
format: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
}
} else {
err();
}
},
custom: function (attr, conf, err, done) {
conf.customValidator.call(this, err, done);
},
uniqueness: function (attr, conf, err, done) {
var cond = {where: {}};
cond.where[attr] = this[attr];
this.constructor.all(cond, function (error, found) {
if (found.length > 1) {
err();
} else if (found.length === 1 && found[0].id !== this.id) {
err();
}
done();
}.bind(this));
/**
* Presence validator
*/
function validatePresence(attr, conf, err) {
if (blank(this[attr])) {
err();
}
};
}
/**
* Length validator
*/
function validateLength(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
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 validator
*/
function validateNumericality(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] !== 'number') {
return err('number');
}
if (conf.int && this[attr] !== Math.round(this[attr])) {
return err('int');
}
}
/**
* Inclusion validator
*/
function validateInclusion(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
if (!~conf.in.indexOf(this[attr])) {
err()
}
}
/**
* Exclusion validator
*/
function validateExclusion(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
if (~conf.in.indexOf(this[attr])) {
err()
}
}
/**
* Format validator
*/
function validateFormat(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
}
} else {
err();
}
}
/**
* Custom validator
*/
function validateCustom(attr, conf, err, done) {
conf.customValidator.call(this, err, done);
}
/**
* Uniqueness validator
*/
function validateUniqueness(attr, conf, err, done) {
var cond = {where: {}};
cond.where[attr] = this[attr];
this.constructor.all(cond, function (error, found) {
if (found.length > 1) {
err();
} else if (found.length === 1 && found[0].id !== this.id) {
err();
}
done();
}.bind(this));
}
var validators = {
presence: validatePresence,
length: validateLength,
numericality: validateNumericality,
inclusion: validateInclusion,
exclusion: validateExclusion,
format: validateFormat,
custom: validateCustom,
uniqueness: validateUniqueness
};
function getConfigurator(name, opts) {
return function () {
@ -94,6 +257,25 @@ function getConfigurator(name, opts) {
};
}
/**
* This method performs validation, triggers validation hooks.
* Before validation `obj.errors` collection cleaned.
* Each validation can add errors to `obj.errors` collection.
* If collection is not blank, validation failed.
*
* @warning This method can be called as sync only when no async validation configured. It's strongly recommended to run all validations as asyncronous.
*
* @param {Function} callback called with (valid)
* @return {Boolean} true if no async validation configured and all passed
*
* @example ExpressJS controller: render user if valid, show flash otherwise
* ```
* user.isValid(function (valid) {
* if (valid) res.render({user: user});
* else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users');
* });
* ```
*/
Validatable.prototype.isValid = function (callback) {
var valid = true, inst = this, wait = 0, async = false;
@ -269,6 +451,13 @@ function nullCheck(attr, conf, err) {
return false;
}
/**
* Return true when v is undefined, blank array, null or empty string
* otherwise returns false
*
* @param {Mix} v
* @returns {Boolean} whether `v` blank or not
*/
function blank(v) {
if (typeof v === 'undefined') return true;
if (v instanceof Array && v.length === 0) return true;