Document
This commit is contained in:
parent
30d0818eed
commit
6ee7de0716
9
index.js
9
index.js
|
@ -1,11 +1,18 @@
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
exports.Schema = require('./lib/schema').Schema;
|
exports.Schema = require('./lib/schema').Schema;
|
||||||
exports.AbstractClass = require('./lib/abstract-class').AbstractClass;
|
exports.AbstractClass = require('./lib/abstract-class').AbstractClass;
|
||||||
exports.Validatable = require('./lib/validatable').Validatable;
|
exports.Validatable = require('./lib/validatable').Validatable;
|
||||||
|
|
||||||
|
exports.init = function () {
|
||||||
|
if (!global.railway) return;
|
||||||
|
railway.orm = exports;
|
||||||
|
require('./lib/railway');
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.versions.node < '0.6' || true) {
|
if (process.versions.node < '0.6') {
|
||||||
exports.version = JSON.parse(fs.readFileSync(__dirname + '/package.json')).version;
|
exports.version = JSON.parse(fs.readFileSync(__dirname + '/package.json')).version;
|
||||||
} else {
|
} else {
|
||||||
exports.version = require('../package').version;
|
exports.version = require('../package').version;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Module deps
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
var Validatable = require('./validatable').Validatable;
|
|
||||||
var Hookable = require('./hookable').Hookable;
|
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var jutil = require('./jutil');
|
var jutil = require('./jutil');
|
||||||
|
var Validatable = require('./validatable').Validatable;
|
||||||
|
var Hookable = require('./hookable').Hookable;
|
||||||
var DEFAULT_CACHE_LIMIT = 1000;
|
var DEFAULT_CACHE_LIMIT = 1000;
|
||||||
|
|
||||||
exports.AbstractClass = AbstractClass;
|
exports.AbstractClass = AbstractClass;
|
||||||
|
@ -13,7 +13,15 @@ jutil.inherits(AbstractClass, Validatable);
|
||||||
jutil.inherits(AbstractClass, Hookable);
|
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) {
|
function AbstractClass(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -99,6 +107,10 @@ function AbstractClass(data) {
|
||||||
AbstractClass.setter = {};
|
AbstractClass.setter = {};
|
||||||
AbstractClass.getter = {};
|
AbstractClass.getter = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} prop - property name
|
||||||
|
* @param {Object} params - various property configuration
|
||||||
|
*/
|
||||||
AbstractClass.defineProperty = function (prop, params) {
|
AbstractClass.defineProperty = function (prop, params) {
|
||||||
this.schema.defineProperty(this.modelName, 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 data [optional]
|
||||||
* @param callback(err, obj)
|
* @param callback(err, obj)
|
||||||
|
* callback called with arguments:
|
||||||
|
*
|
||||||
|
* - err (null or Error)
|
||||||
|
* - instance (null or Model)
|
||||||
*/
|
*/
|
||||||
AbstractClass.create = function (data, callback) {
|
AbstractClass.create = function (data, callback) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
if (stillConnecting(this.schema, this, arguments)) return;
|
||||||
|
@ -178,6 +196,9 @@ function stillConnecting(schema, obj, args) {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update or insert
|
||||||
|
*/
|
||||||
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
|
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
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) {
|
AbstractClass.exists = function exists(id, cb) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
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) {
|
AbstractClass.find = function find(id, cb) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
if (stillConnecting(this.schema, this, arguments)) return;
|
||||||
|
|
||||||
|
@ -236,9 +269,20 @@ AbstractClass.find = function find(id, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query collection of objects
|
* Find all instances of Model, matched by query
|
||||||
* @param params {where: {}, order: '', limit: 1, offset: 0,...}
|
* make sure you have marked as `index: true` fields for filter or sort
|
||||||
* @param cb (err, array of AbstractClass)
|
*
|
||||||
|
* @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) {
|
AbstractClass.all = function all(params, cb) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
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) {
|
AbstractClass.findOne = function findOne(params, cb) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
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) {
|
AbstractClass.destroyAll = function destroyAll(cb) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
if (stillConnecting(this.schema, this, arguments)) return;
|
||||||
|
|
||||||
|
@ -302,6 +356,12 @@ AbstractClass.destroyAll = function destroyAll(cb) {
|
||||||
}.bind(this));
|
}.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) {
|
AbstractClass.count = function (where, cb) {
|
||||||
if (stillConnecting(this.schema, this, arguments)) return;
|
if (stillConnecting(this.schema, this, arguments)) return;
|
||||||
|
|
||||||
|
@ -312,6 +372,11 @@ AbstractClass.count = function (where, cb) {
|
||||||
this.schema.adapter.count(this.modelName, cb, where);
|
this.schema.adapter.count(this.modelName, cb, where);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return string representation of class
|
||||||
|
*
|
||||||
|
* @override default toString method
|
||||||
|
*/
|
||||||
AbstractClass.toString = function () {
|
AbstractClass.toString = function () {
|
||||||
return '[Model ' + this.modelName + ']';
|
return '[Model ' + this.modelName + ']';
|
||||||
}
|
}
|
||||||
|
@ -393,14 +458,22 @@ AbstractClass.prototype.isNewRecord = function () {
|
||||||
return !this.id;
|
return !this.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return adapter of current record
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
AbstractClass.prototype._adapter = function () {
|
AbstractClass.prototype._adapter = function () {
|
||||||
return this.constructor.schema.adapter;
|
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) {
|
AbstractClass.prototype.toObject = function (onlySchema) {
|
||||||
var data = {};
|
var data = {};
|
||||||
var ds = this.constructor.schema.definitions[this.constructor.modelName];
|
var ds = this.constructor.schema.definitions[this.constructor.modelName];
|
||||||
|
@ -412,6 +485,11 @@ AbstractClass.prototype.toObject = function (onlySchema) {
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object from persistence
|
||||||
|
*
|
||||||
|
* @triggers `destroy` hook (async) before and after destroying object
|
||||||
|
*/
|
||||||
AbstractClass.prototype.destroy = function (cb) {
|
AbstractClass.prototype.destroy = function (cb) {
|
||||||
if (stillConnecting(this.constructor.schema, this, arguments)) return;
|
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 = {};
|
||||||
data[name] = value;
|
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) {
|
AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
|
||||||
if (stillConnecting(this.constructor.schema, this, arguments)) return;
|
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
|
* Checks is property changed based on current property and initial value
|
||||||
* @param {attr} String - property name
|
*
|
||||||
|
* @param {String} attr - property name
|
||||||
* @return Boolean
|
* @return Boolean
|
||||||
*/
|
*/
|
||||||
AbstractClass.prototype.propertyChanged = function (attr) {
|
AbstractClass.prototype.propertyChanged = function propertyChanged(attr) {
|
||||||
return this['_' + attr] !== this[attr + '_was'];
|
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;
|
if (stillConnecting(this.constructor.schema, this, arguments)) return;
|
||||||
|
|
||||||
var obj = getCached(this.constructor, this.id);
|
var obj = getCached(this.constructor, this.id);
|
||||||
if (obj) {
|
if (obj) obj.reset();
|
||||||
obj.reset();
|
this.constructor.find(this.id, callback);
|
||||||
}
|
|
||||||
this.constructor.find(this.id, cb);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset dirty attributes
|
||||||
|
*
|
||||||
|
* this method does not perform any database operation it just reset object to it's
|
||||||
|
* initial state
|
||||||
|
*/
|
||||||
AbstractClass.prototype.reset = function () {
|
AbstractClass.prototype.reset = function () {
|
||||||
var obj = this;
|
var obj = this;
|
||||||
Object.keys(obj).forEach(function (k) {
|
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 methodName = params.as; // or pluralize(anotherClass.modelName)
|
||||||
var fk = params.foreignKey;
|
var fk = params.foreignKey;
|
||||||
// each instance of this class should have method named
|
// 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) {
|
AbstractClass.belongsTo = function (anotherClass, params) {
|
||||||
var methodName = params.as;
|
var methodName = params.as;
|
||||||
var fk = params.foreignKey;
|
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) {
|
AbstractClass.scope = function (name, params) {
|
||||||
defineScope(this, this, 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) {
|
function isdef(s) {
|
||||||
var undef;
|
var undef;
|
||||||
return s !== 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) {
|
function merge(base, update) {
|
||||||
base = base || {};
|
base = base || {};
|
||||||
if (update) {
|
if (update) {
|
||||||
|
@ -707,6 +837,13 @@ function merge(base, update) {
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define readonly property on object
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Mixed} value
|
||||||
|
*/
|
||||||
function defineReadonlyProp(obj, key, value) {
|
function defineReadonlyProp(obj, key, value) {
|
||||||
Object.defineProperty(obj, key, {
|
Object.defineProperty(obj, key, {
|
||||||
writable: false,
|
writable: false,
|
||||||
|
@ -716,11 +853,17 @@ function defineReadonlyProp(obj, key, value) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add object to cache
|
||||||
|
*/
|
||||||
function addToCache(constr, obj) {
|
function addToCache(constr, obj) {
|
||||||
touchCache(constr, obj.id);
|
touchCache(constr, obj.id);
|
||||||
constr.cache[obj.id] = obj;
|
constr.cache[obj.id] = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renew object position in LRU cache index
|
||||||
|
*/
|
||||||
function touchCache(constr, id) {
|
function touchCache(constr, id) {
|
||||||
var cacheLimit = constr.CACHE_LIMIT || DEFAULT_CACHE_LIMIT;
|
var cacheLimit = constr.CACHE_LIMIT || DEFAULT_CACHE_LIMIT;
|
||||||
|
|
||||||
|
@ -734,16 +877,32 @@ function touchCache(constr, id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve cached object
|
||||||
|
*/
|
||||||
function getCached(constr, id) {
|
function getCached(constr, id) {
|
||||||
if (id) touchCache(constr, id);
|
if (id) touchCache(constr, id);
|
||||||
return id && constr.cache[id];
|
return id && constr.cache[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache (fully)
|
||||||
|
*
|
||||||
|
* removes both cache and LRU index
|
||||||
|
*
|
||||||
|
* @param {Class} constr - class constructor
|
||||||
|
*/
|
||||||
function clearCache(constr) {
|
function clearCache(constr) {
|
||||||
constr.cache = {};
|
constr.cache = {};
|
||||||
constr.mru = [];
|
constr.mru = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove object from cache
|
||||||
|
*
|
||||||
|
* @param {Class} constr
|
||||||
|
* @param {id} id
|
||||||
|
*/
|
||||||
function removeFromCache(constr, id) {
|
function removeFromCache(constr, id) {
|
||||||
var ind = constr.mru.indexOf(id);
|
var ind = constr.mru.indexOf(id);
|
||||||
if (!~ind) constr.mru.splice(ind, 1);
|
if (!~ind) constr.mru.splice(ind, 1);
|
||||||
|
|
|
@ -35,6 +35,9 @@ exports.initialize = function initializeSchema(schema, callback) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL adapter
|
||||||
|
*/
|
||||||
function MySQL(client) {
|
function MySQL(client) {
|
||||||
this._models = {};
|
this._models = {};
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
|
@ -54,7 +54,7 @@ BridgeToRedis.prototype.save = function (model, data, callback) {
|
||||||
|
|
||||||
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback) {
|
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback) {
|
||||||
var i = this.indexes[model];
|
var i = this.indexes[model];
|
||||||
var schedule = [];
|
var schedule = [['sadd', 's:' + model, id]];
|
||||||
Object.keys(data).forEach(function (key) {
|
Object.keys(data).forEach(function (key) {
|
||||||
if (i[key]) {
|
if (i[key]) {
|
||||||
schedule.push([
|
schedule.push([
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
198
lib/schema.js
198
lib/schema.js
|
@ -17,10 +17,30 @@ exports.Schema = Schema;
|
||||||
var slice = Array.prototype.slice;
|
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 name - type of schema adapter (mysql, mongoose, sequelize, redis)
|
||||||
* @param settings - any database-specific settings which we need to
|
* @param settings - any database-specific settings which we need to
|
||||||
* establish connection (of course it depends on specific adapter)
|
* 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) {
|
function Schema(name, settings) {
|
||||||
var schema = this;
|
var schema = this;
|
||||||
|
@ -73,65 +93,40 @@ function Schema(name, settings) {
|
||||||
|
|
||||||
util.inherits(Schema, require('events').EventEmitter);
|
util.inherits(Schema, require('events').EventEmitter);
|
||||||
|
|
||||||
function Text() {
|
function Text() {}
|
||||||
}
|
|
||||||
Schema.Text = 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
|
* Define class
|
||||||
* @param className
|
*
|
||||||
* @param properties - hash of class properties in format
|
* @param {String} className
|
||||||
* {property: Type, property2: Type2, ...}
|
* @param {Object} properties - hash of class properties in format
|
||||||
* or
|
* `{property: Type, property2: Type2, ...}`
|
||||||
* {property: {type: Type}, property2: {type: Type2}, ...}
|
* or
|
||||||
* @param settings - other configuration of class
|
* `{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) {
|
Schema.prototype.define = function defineClass(className, properties, settings) {
|
||||||
var schema = this;
|
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) {
|
Schema.prototype.tableName = function (modelName) {
|
||||||
return this.definitions[modelName].settings.table = this.definitions[modelName].settings.table || 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) {
|
Schema.prototype.defineForeignKey = function defineForeignKey(className, key) {
|
||||||
// return if already defined
|
// return if already defined
|
||||||
if (this.definitions[className].properties[key]) return;
|
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() {
|
Schema.prototype.disconnect = function disconnect() {
|
||||||
if (typeof this.adapter.disconnect === 'function') {
|
if (typeof this.adapter.disconnect === 'function') {
|
||||||
this.adapter.disconnect();
|
this.adapter.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define hidden property
|
||||||
|
*/
|
||||||
function hiddenProperty(where, property, value) {
|
function hiddenProperty(where, property, value) {
|
||||||
Object.defineProperty(where, property, {
|
Object.defineProperty(where, property, {
|
||||||
writable: false,
|
writable: false,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
module.exports = BaseSQL;
|
module.exports = BaseSQL;
|
||||||
|
|
||||||
function BaseSQL() {}
|
/**
|
||||||
|
* Base SQL class
|
||||||
|
*/
|
||||||
|
function BaseSQL() {
|
||||||
|
}
|
||||||
|
|
||||||
BaseSQL.prototype.query = function () {
|
BaseSQL.prototype.query = function () {
|
||||||
throw new Error('query method should be declared in adapter');
|
throw new Error('query method should be declared in adapter');
|
||||||
|
|
|
@ -1,92 +1,255 @@
|
||||||
exports.Validatable = Validatable;
|
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() {
|
function Validatable() {
|
||||||
// validatable class
|
// 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');
|
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');
|
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');
|
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');
|
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');
|
Validatable.validatesExclusionOf = getConfigurator('exclusion');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate format
|
||||||
|
*
|
||||||
|
* Default error message: is invalid
|
||||||
|
*
|
||||||
|
* @nocode
|
||||||
|
* @see helper/validateFormat
|
||||||
|
*/
|
||||||
Validatable.validatesFormatOf = getConfigurator('format');
|
Validatable.validatesFormatOf = getConfigurator('format');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate using custom validator
|
||||||
|
*
|
||||||
|
* Default error message: is invalid
|
||||||
|
*
|
||||||
|
* @nocode
|
||||||
|
* @see helper/validateCustom
|
||||||
|
*/
|
||||||
Validatable.validate = getConfigurator('custom');
|
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});
|
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});
|
Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
|
||||||
|
|
||||||
// implementation of validators
|
// 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) {
|
* Presence validator
|
||||||
err('min');
|
*/
|
||||||
}
|
function validatePresence(attr, conf, err) {
|
||||||
if (conf.max && len > conf.max) {
|
if (blank(this[attr])) {
|
||||||
err('max');
|
err();
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
function getConfigurator(name, opts) {
|
||||||
return function () {
|
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) {
|
Validatable.prototype.isValid = function (callback) {
|
||||||
var valid = true, inst = this, wait = 0, async = false;
|
var valid = true, inst = this, wait = 0, async = false;
|
||||||
|
|
||||||
|
@ -269,6 +451,13 @@ function nullCheck(attr, conf, err) {
|
||||||
return false;
|
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) {
|
function blank(v) {
|
||||||
if (typeof v === 'undefined') return true;
|
if (typeof v === 'undefined') return true;
|
||||||
if (v instanceof Array && v.length === 0) return true;
|
if (v instanceof Array && v.length === 0) return true;
|
||||||
|
|
Loading…
Reference in New Issue