/**
* Module dependencies
*/
var util = require('util');
1var jutil = require('./jutil');
1var Validatable = require('./validatable').Validatable;
1var Hookable = require('./hookable').Hookable;
1var DEFAULT_CACHE_LIMIT = 1000;
1
exports.AbstractClass = AbstractClass;
1
jutil.inherits(AbstractClass, Validatable);
1jutil.inherits(AbstractClass, Hookable);
1
/**
* 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) {
this._initProperties(data, true);
152}
AbstractClass.prototype._initProperties = function (data, applySetters) {
var self = this;
267 var ctor = this.constructor;
267 var ds = ctor.schema.definitions[ctor.modelName];
267 var properties = ds.properties;
267 data = data || {};
267
if (data.id) {
defineReadonlyProp(this, 'id', data.id);
109 }
Object.defineProperty(this, 'cachedRelations', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
267
Object.keys(properties).forEach(function (attr) {
var _attr = '_' + attr,
attr_was = attr + '_was';
1696
// Hidden property to store currrent value
Object.defineProperty(this, _attr, {
writable: true,
enumerable: false,
configurable: true,
value: isdef(data[attr]) ? data[attr] :
(isdef(this[attr]) ? this[attr] : (
getDefault(attr)
))
});
1696
// Public setters and getters
Object.defineProperty(this, attr, {
get: function () {
if (ctor.getter[attr]) {
return ctor.getter[attr].call(this);
} else {
return this[_attr];
601 }
},
set: function (value) {
if (ctor.setter[attr]) {
ctor.setter[attr].call(this, value);
1 } else {
this[_attr] = value;
9 }
},
configurable: true,
enumerable: true
});
1696
if (data.hasOwnProperty(attr)) {
if (applySetters && ctor.setter[attr]) {
ctor.setter[attr].call(this, data[attr]);
3 }
// Getter for initial property
Object.defineProperty(this, attr_was, {
writable: true,
value: this[_attr],
configurable: true,
enumerable: false
});
156 }
}.bind(this));
267
function getDefault(attr) {
var def = properties[attr]['default']
if (isdef(def)) {
if (typeof def === 'function') {
return def();
102 } else {
return def;
117 }
} else {
return null;
688 }
}
this.trigger("initialize");
267};
1
AbstractClass.setter = {};
1AbstractClass.getter = {};
1
/**
* @param {String} prop - property name
* @param {Object} params - various property configuration
*/
AbstractClass.defineProperty = function (prop, params) {
this.schema.defineProperty(this.modelName, prop, params);
};
1
AbstractClass.whatTypeName = function (propName) {
var ds = this.schema.definitions[this.modelName];
4 return ds.properties[propName].type.name;
4};
1
AbstractClass.prototype.whatTypeName = function (propName) {
return this.constructor.whatTypeName(propName);
2};
1
/**
* 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;
39
var modelName = this.modelName;
39
if (typeof data === 'function') {
callback = data;
5 data = {};
5 }
if (typeof callback !== 'function') {
callback = function () {};
1 }
var obj = null;
39 // if we come from save
if (data instanceof AbstractClass && !data.id) {
obj = data;
5 data = obj.toObject(true);
5 this.prototype._initProperties.call(obj, data, false);
5 create();
5 } else {
obj = new this(data);
34 data = obj.toObject(true);
34
// validation required
obj.isValid(function (valid) {
if (!valid) {
callback(new Error('Validation error'), obj);
} else {
create();
34 }
});
34 }
function create() {
obj.trigger('create', function (done) {
this._adapter().create(modelName, data, function (err, id) {
if (id) {
defineReadonlyProp(obj, 'id', id);
39 addToCache(this.constructor, obj);
39 }
done.call(this, function () {
if (callback) {
callback(err, obj);
39 }
});
39 }.bind(this));
39 });
39 }
};
1
function stillConnecting(schema, obj, args) {
if (schema.connected) return false;
var method = args.callee;
schema.on('connected', function () {
method.apply(obj, [].slice.call(args));
});
return true;
};
1
/**
* Update or insert
*/
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
1
var Model = this;
1 if (!data.id) return this.create(data, callback);
if (this.schema.adapter.updateOrCreate) {
var inst = new Model(data);
this.schema.adapter.updateOrCreate(Model.modelName, inst.toObject(), function (err, data) {
var obj;
if (data) {
inst._initProperties(data);
obj = inst;
} else {
obj = null;
}
if (obj) {
addToCache(Model, obj);
}
callback(err, obj);
});
} else {
this.find(data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
inst.updateAttributes(data, callback);
} else {
var obj = new Model(data);
obj.save(data, callback);
}
});
}
};
1
/**
* 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;
3
if (id) {
this.schema.adapter.exists(this.modelName, id, cb);
3 } else {
cb(new Error('Model::exists requires positive id argument'));
}
};
1
/**
* 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;
7
this.schema.adapter.find(this.modelName, id, function (err, data) {
var obj = null;
7 if (data) {
var cached = getCached(this, data.id);
6 if (cached) {
obj = cached;
substractDirtyAttributes(obj, data);
// maybe just obj._initProperties(data); instead of
this.prototype._initProperties.call(obj, data);
} else {
data.id = id;
6 obj = new this();
6 obj._initProperties(data, false);
6 addToCache(this, id);
6 }
}
cb(err, obj);
7 }.bind(this));
7};
1
/**
* 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;
23
if (arguments.length === 1) {
cb = params;
2 params = null;
2 }
var constr = this;
23 this.schema.adapter.all(this.modelName, params, function (err, data) {
var collection = null;
23 if (data && data.map) {
collection = data.map(function (d) {
var obj = null;
99 // do not create different instances for the same object
var cached = getCached(constr, d.id);
99 if (cached) {
obj = cached;
// keep dirty attributes untouthed(remove from dataset)
substractDirtyAttributes(obj, d);
// maybe just obj._initProperties(d);
constr.prototype._initProperties.call(obj, d);
} else {
obj = new constr;
99 obj._initProperties(d, false);
99 if (obj.id) addToCache(constr, obj);
99 }
return obj;
99 });
23 cb(err, collection);
23 }
});
23};
1
/**
* 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;
3
if (typeof params === 'function') {
cb = params;
1 params = {};
1 }
params.limit = 1;
3 this.all(params, function (err, collection) {
if (err || !collection || !collection.length > 0) return cb(err);
2 cb(err, collection[0]);
2 });
3};
1
function substractDirtyAttributes(object, data) {
Object.keys(object.toObject()).forEach(function (attr) {
if (data.hasOwnProperty(attr) && object.propertyChanged(attr)) {
delete data[attr];
}
});
}
/**
* Destroy all records
* @param {Function} cb - callback called with (err)
*/
AbstractClass.destroyAll = function destroyAll(cb) {
if (stillConnecting(this.schema, this, arguments)) return;
3
this.schema.adapter.destroyAll(this.modelName, function (err) {
clearCache(this);
3 cb(err);
3 }.bind(this));
3};
1
/**
* 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;
3
if (typeof where === 'function') {
cb = where;
2 where = null;
2 }
this.schema.adapter.count(this.modelName, cb, where);
3};
1
/**
* Return string representation of class
*
* @override default toString method
*/
AbstractClass.toString = function () {
return '[Model ' + this.modelName + ']';
}
/**
* Save instance. When instance haven't id, create method called instead.
* Triggers: validate, save, update | create
* @param options {validate: true, throws: false} [optional]
* @param callback(err, obj)
*/
AbstractClass.prototype.save = function (options, callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
8
if (typeof options == 'function') {
callback = options;
8 options = {};
8 }
callback = callback || function () {};
8 options = options || {};
8
if (!('validate' in options)) {
options.validate = true;
8 }
if (!('throws' in options)) {
options.throws = false;
8 }
if (options.validate) {
this.isValid(function (valid) {
if (valid) {
save.call(this);
8 } else {
var err = new Error('Validation error');
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, this);
}
}.bind(this));
8 } else {
save.call(this);
}
function save() {
this.trigger('save', function (saveDone) {
var modelName = this.constructor.modelName;
8 var data = this.toObject(true);
8 var inst = this;
8 if (inst.id) {
inst.trigger('update', function (updateDone) {
inst._adapter().save(modelName, data, function (err) {
if (err) {
console.log(err);
} else {
inst._initProperties(data, false);
3 }
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
3 });
3 });
3 });
3 }, data);
3 } else {
inst.constructor.create(inst, function (err) {
saveDone.call(inst, function () {
callback(err, inst);
5 });
5 });
5 }
});
8 }
};
1
AbstractClass.prototype.isNewRecord = function () {
return !this.id;
3};
1
/**
* Return adapter of current record
* @private
*/
AbstractClass.prototype._adapter = function () {
return this.constructor.schema.adapter;
45};
1
/**
* 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 = {};
47 var ds = this.constructor.schema.definitions[this.constructor.modelName];
47 var properties = ds.properties;
47 // weird
Object.keys(onlySchema ? properties : this).concat(['id']).forEach(function (property) {
data[property] = this[property];
355 }.bind(this));
47 return data;
47};
1
/**
* 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;
1
this.trigger('destroy', function (destroyed) {
this._adapter().destroy(this.constructor.modelName, this.id, function (err) {
removeFromCache(this.constructor, this.id);
1 destroyed(function () {
cb && cb(err);
1 });
1 }.bind(this));
1 });
1};
1
/**
* 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) {
var data = {};
1 data[name] = value;
1 this.updateAttributes(data, callback);
1};
1
/**
* 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;
2
var inst = this;
2 var model = this.constructor.modelName;
2
// update instance's properties
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
3 });
2
inst.isValid(function (valid) {
if (!valid) {
if (cb) {
cb(new Error('Validation error'));
}
} else {
update();
2 }
});
2
function update() {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (done) {
Object.keys(data).forEach(function (key) {
data[key] = inst[key];
3 });
2
inst._adapter().updateAttributes(model, inst.id, data, function (err) {
if (!err) {
inst._initProperties(data, false);
2 /*
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
Object.defineProperty(inst, key + '_was', {
writable: false,
configurable: true,
enumerable: false,
value: data[key]
});
});
*/
}
done.call(inst, function () {
saveDone.call(inst, function () {
cb(err, inst);
2 });
2 });
2 });
2 }, data);
2 });
2 }
};
1
AbstractClass.prototype.fromObject = function (obj) {
Object.keys(obj).forEach(function (key) {
this[key] = obj[key];
}.bind(this));
};
1
/**
* Checks is property changed based on current property and initial value
*
* @param {String} attr - property name
* @return Boolean
*/
AbstractClass.prototype.propertyChanged = function propertyChanged(attr) {
return this['_' + attr] !== this[attr + '_was'];
11};
1
/**
* 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;
2
var obj = getCached(this.constructor, this.id);
2 if (obj) obj.reset();
2 this.constructor.find(this.id, callback);
2};
1
/**
* 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) {
if (k !== 'id' && !obj.constructor.schema.definitions[obj.constructor.modelName].properties[k]) {
delete obj[k];
}
if (obj.propertyChanged(k)) {
obj[k] = obj[k + '_was'];
}
});
};
1
/**
* 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;
1 // each instance of this class should have method named
// pluralize(anotherClass.modelName)
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
defineScope(this.prototype, anotherClass, methodName, function () {
var x = {};
8 x[fk] = this.id;
8 return {where: x};
8 }, {
find: find,
destroy: destroy
});
1
// obviously, anotherClass should have attribute called `fk`
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
1
function find(id, cb) {
anotherClass.find(id, function (err, inst) {
if (err) return cb(err);
if (!inst) return cb(new Error('Not found'));
if (inst[fk] == this.id) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
}
function destroy(id, cb) {
this.find(id, function (err, inst) {
if (err) return cb(err);
if (inst) {
inst.destroy(cb);
} else {
cb(new Error('Not found'));
}
});
}
};
1
/**
* 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;
2 var fk = params.foreignKey;
2
this.schema.defineForeignKey(this.modelName, fk);
2 this.prototype['__finders__'] = this.prototype['__finders__'] || {}
this.prototype['__finders__'][methodName] = function (id, cb) {
anotherClass.find(id, function (err,inst) {
if (err) return cb(err);
if (inst[fk] === this.id) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
}
this.prototype[methodName] = function (p) {
if (p instanceof AbstractClass) { // acts as setter
this[fk] = p.id;
this.cachedRelations[methodName] = p;
} else if (typeof p === 'function') { // acts as async getter
this.__finders__[methodName](this[fk], p);
return this[fk];
} else if (typeof p === 'undefined') { // acts as sync getter
return this[fk];
2 } else { // setter
this[fk] = p;
1 }
};
2
};
1
/**
* Define scope
* TODO: describe behavior and usage examples
*/
AbstractClass.scope = function (name, params) {
defineScope(this, this, name, params);
1};
1
function defineScope(cls, targetClass, name, params, methods) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
1 }
// anly make sence to add scope in meta if base and target classes
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
1 } else {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
1 }
}
Object.defineProperty(cls, name, {
enumerable: false,
configurable: true,
get: function () {
var f = function caller(cond, cb) {
var actualCond;
1 if (arguments.length === 1) {
actualCond = {};
1 cb = cond;
1 } else if (arguments.length === 2) {
actualCond = cond;
} else {
throw new Error('Method only can be called with one or two arguments');
}
return targetClass.all(mergeParams(actualCond, caller._scope), cb);
1 };
12 f._scope = typeof params === 'function' ? params.call(this) : params;
12 f.build = build;
12 f.create = create;
12 f.destroyAll = destroyAll;
12 for (var i in methods) {
f[i] = methods[i].bind(this);
16 }
// define sub-scopes
Object.keys(targetClass._scopeMeta).forEach(function (name) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
mergeParams(f._scope, targetClass._scopeMeta[name]);
3 return f;
3 }
});
7 }.bind(this));
12 return f;
12 }
});
2
// and it should have create/build methods with binded thisModelNameId param
function build(data) {
data = data || {};
3 return new targetClass(mergeParams(this._scope, {where:data}).where);
3 }
function create(data, cb) {
if (typeof data === 'function') {
cb = data;
2 data = {};
2 }
this.build(data).save(cb);
2 }
function destroyAll(id, cb) {
// implement me
}
function mergeParams(base, update) {
if (update.where) {
base.where = merge(base.where, update.where);
7 }
// overwrite order
if (update.order) {
base.order = update.order;
}
return base;
7
}
}
/**
* Check whether `s` is not undefined
* @param {Mixed} s
* @return {Boolean} s is undefined
*/
function isdef(s) {
var undef;
3603 return s !== undef;
3603}
/**
* 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 || {};
7 if (update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
4 });
7 }
return base;
7}
/**
* 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,
enumerable: true,
configurable: true,
value: value
});
148}
/**
* Add object to cache
*/
function addToCache(constr, obj) {
return;
144 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;
101
var ind = constr.mru.indexOf(id);
101 if (~ind) constr.mru.splice(ind, 1);
101 if (constr.mru.length >= cacheLimit * 2) {
for (var i = 0; i < cacheLimit;i += 1) {
delete constr.cache[constr.mru[i]];
}
constr.mru.splice(0, cacheLimit);
}
}
/**
* Retrieve cached object
*/
function getCached(constr, id) {
if (id) touchCache(constr, id);
107 return id && constr.cache[id];
107}
/**
* Clear cache (fully)
*
* removes both cache and LRU index
*
* @param {Class} constr - class constructor
*/
function clearCache(constr) {
constr.cache = {};
3 constr.mru = [];
3}
/**
* Remove object from cache
*
* @param {Class} constr
* @param {id} id
*/
function removeFromCache(constr, id) {
var ind = constr.mru.indexOf(id);
1 if (!~ind) constr.mru.splice(ind, 1);
1 delete constr.cache[id];
1}
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
};
1
/**
* Validate presence. This validation fails when validated field is blank.
*
* Default error message "can't be blank"
*
* @example presence of title
* ```
* Post.validatesPresenceOf('title');
* ```
* @example with custom message
* ```
* Post.validatesPresenceOf('title', {message: 'Can not be blank'});
* ```
*
* @sync
*
* @nocode
* @see helper/validatePresence
*/
Validatable.validatesPresenceOf = getConfigurator('presence');
1
/**
* Validate length. Three kinds of validations: min, max, is.
*
* Default error messages:
*
* - min: too short
* - max: too long
* - is: length is wrong
*
* @example length validations
* ```
* User.validatesLengthOf('password', {min: 7});
* User.validatesLengthOf('email', {max: 100});
* User.validatesLengthOf('state', {is: 2});
* User.validatesLengthOf('nick', {min: 3, max: 15});
* ```
* @example length validations with custom error messages
* ```
* User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}});
* User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}});
* ```
*
* @sync
* @nocode
* @see helper/validateLength
*/
Validatable.validatesLengthOf = getConfigurator('length');
1
/**
* Validate numericality.
*
* @example
* ```
* User.validatesNumericalityOf('age', { message: { number: '...' }});
* 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');
1
/**
* Validate inclusion in set
*
* @example
* ```
* User.validatesInclusionOf('gender', {in: ['male', 'female']});
* User.validatesInclusionOf('role', {
* in: ['admin', 'moderator', 'user'], message: 'is not allowed'
* });
* ```
*
* Default error message: is not included in the list
*
* @sync
* @nocode
* @see helper/validateInclusion
*/
Validatable.validatesInclusionOf = getConfigurator('inclusion');
1
/**
* Validate exclusion
*
* @example `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});`
*
* Default error message: is reserved
*
* @nocode
* @see helper/validateExclusion
*/
Validatable.validatesExclusionOf = getConfigurator('exclusion');
1
/**
* Validate format
*
* Default error message: is invalid
*
* @nocode
* @see helper/validateFormat
*/
Validatable.validatesFormatOf = getConfigurator('format');
1
/**
* Validate using custom validator
*
* Default error message: is invalid
*
* @nocode
* @see helper/validateCustom
*/
Validatable.validate = getConfigurator('custom');
1
/**
* Validate using custom async validator
*
* Default error message: is invalid
*
* @async
* @nocode
* @see helper/validateCustom
*/
Validatable.validateAsync = getConfigurator('custom', {async: true});
1
/**
* Validate uniqueness
*
* Default error message: is not unique
*
* @async
* @nocode
* @see helper/validateUniqueness
*/
Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
1
// implementation of validators
/**
* 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);
32}
/**
* 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
};
1
function getConfigurator(name, opts) {
return function () {
9 configure(this, name, arguments, opts);
1 };
}
/**
* 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;
44
// exit with success when no errors
if (!this.constructor._validations) {
cleanErrors(this);
12 if (callback) {
callback(valid);
12 }
return valid;
12 }
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: new Errors
});
32
this.trigger('validation', function (validationsDone) {
var inst = this;
32 this.constructor._validations.forEach(function (v) {
if (v[2] && v[2].async) {
async = true;
32 wait += 1;
32 validationFailed(inst, v, done);
32 } else {
if (validationFailed(inst, v)) {
valid = false;
}
}
});
32
if (!async) {
validationsDone();
}
var asyncFail = false;
32 function done(fail) {
asyncFail = asyncFail || fail;
32 if (--wait === 0 && callback) {
validationsDone.call(inst, function () {
if( valid && !asyncFail ) cleanErrors(inst);
32 callback(valid && !asyncFail);
32 });
32 }
}
});
32
if (!async) {
if (valid) cleanErrors(this);
if (callback) callback(valid);
return valid;
} else {
// in case of async validation we should return undefined here,
// because not all validations are finished yet
return;
32 }
};
1
function cleanErrors(inst) {
Object.defineProperty(inst, 'errors', {
enumerable: false,
configurable: true,
value: false
});
44}
function validationFailed(inst, v, cb) {
var attr = v[0];
32 var conf = v[1];
32 var opts = v[2] || {};
32
if (typeof attr !== 'string') return false;
32
// here we should check skip validation conditions(if, unless)
// that can be specified in conf
if (skipValidation(inst, conf, 'if')) return false;
32 if (skipValidation(inst, conf, 'unless')) return false;
32
var fail = false;
32 var validator = validators[conf.validation];
32 var validatorArguments = [];
32 validatorArguments.push(attr);
32 validatorArguments.push(conf);
32 validatorArguments.push(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;
});
32 if (cb) {
validatorArguments.push(function () {
cb(fail);
32 });
32 }
validator.apply(inst, validatorArguments);
32 return fail;
32}
function skipValidation(inst, conf, kind) {
var doValidate = true;
64 if (typeof conf[kind] === 'function') {
doValidate = conf[kind].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else if (typeof conf[kind] === 'string') {
if (inst.hasOwnProperty(conf[kind])) {
doValidate = inst[conf[kind]];
if (kind === 'unless') doValidate = !doValidate;
} else if (typeof inst[conf[kind]] === 'function') {
doValidate = inst[conf[kind]].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else {
doValidate = kind === 'if';
}
}
return !doValidate;
64}
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',
uniqueness: 'is not unique'
};
1
function nullCheck(attr, conf, err) {
var isNull = this[attr] === null || !(attr in this);
if (isNull) {
if (!conf.allowNull) {
err('null');
}
return true;
} else {
if (blank(this[attr])) {
if (!conf.allowBlank) {
err('blank');
}
return true;
}
}
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;
if (v === null) return true;
if (typeof v == 'string' && v === '') return true;
return false;
}
function configure(cls, validation, args, opts) {
if (!cls._validations) {
Object.defineProperty(cls, '_validations', {
writable: true,
configurable: true,
enumerable: false,
value: []
});
1 }
args = [].slice.call(args);
1 var conf;
1 if (typeof args[args.length - 1] === 'object') {
conf = args.pop();
} else {
conf = {};
1 }
if (validation === 'custom' && typeof args[args.length - 1] === 'function') {
conf.customValidator = args.pop();
1 }
conf.validation = validation;
1 args.forEach(function (attr) {
cls._validations.push([attr, conf, opts]);
1 });
1}
function Errors() {
}
Errors.prototype.add = function (field, message) {
if (!this[field]) {
this[field] = [message];
} else {
this[field].push(message);
}
};
1
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var mongoose = safeRequire('mongoose');
1
exports.initialize = function initializeSchema(schema, callback) {
if (!mongoose) return;
1
if (!schema.settings.url) {
var url = schema.settings.host || 'localhost';
if (schema.settings.port) url += ':' + schema.settings.port;
var auth = '';
if (schema.settings.username) {
auth = schema.settings.username;
if (schema.settings.password) {
auth += ':' + schema.settings.password;
}
}
if (auth) {
url = auth + '@' + url;
}
if (schema.settings.database) {
url += '/' + schema.settings.database;
} else {
url += '/';
}
url = 'mongodb://' + url;
schema.settings.url = url;
}
schema.client = mongoose.connect(schema.settings.url);
1 schema.adapter = new MongooseAdapter(schema.client);
1 process.nextTick(callback);
1};
1
function MongooseAdapter(client) {
this._models = {};
1 this.client = client;
1 this.cache = {};
1}
MongooseAdapter.prototype.define = function (descr) {
var props = {};
3 Object.keys(descr.properties).forEach(function (key) {
props[key] = descr.properties[key].type;
14 if (props[key].name === 'Text') props[key] = String;
14 if (props[key].name === 'Object') props[key] = mongoose.Schema.Types.Mixed;
14 });
3 var schema = new mongoose.Schema(props);
3 this._models[descr.model.modelName] = mongoose.model(descr.model.modelName, schema);
3 this.cache[descr.model.modelName] = {};
3};
1
MongooseAdapter.prototype.defineForeignKey = function (model, key, cb) {
var piece = {};
2 piece[key] = {type: mongoose.Schema.ObjectId, index: true};
2 this._models[model].schema.add(piece);
2 cb(null, String);
2};
1
MongooseAdapter.prototype.setCache = function (model, instance) {
this.cache[model][instance.id] = instance;
};
1
MongooseAdapter.prototype.getCached = function (model, id, cb) {
if (this.cache[model][id]) {
cb(null, this.cache[model][id]);
1 } else {
this._models[model].findById(id, function (err, instance) {
if (err) {
return cb(err);
}
this.cache[model][id] = instance;
15 cb(null, instance);
15 }.bind(this));
15 }
};
1
MongooseAdapter.prototype.create = function (model, data, callback) {
var m = new this._models[model](data);
39 m.save(function (err) {
callback(err, err ? null : m.id);
39 });
39};
1
MongooseAdapter.prototype.save = function (model, data, callback) {
this.getCached(model, data.id, function (err, inst) {
if (err) {
return callback(err);
}
merge(inst, data);
3 inst.save(callback);
3 });
3};
1
MongooseAdapter.prototype.exists = function (model, id, callback) {
delete this.cache[model][id];
3 this.getCached(model, id, function (err, data) {
if (err) {
return callback(err);
}
callback(err, !!data);
3 });
3};
1
MongooseAdapter.prototype.find = function find(model, id, callback) {
delete this.cache[model][id];
7 this.getCached(model, id, function (err, data) {
if (err) {
return callback(err);
}
callback(err, data ? data.toObject() : null);
7 });
7};
1
MongooseAdapter.prototype.destroy = function destroy(model, id, callback) {
this.getCached(model, id, function (err, data) {
if (err) {
return callback(err);
}
if (data) {
data.remove(callback);
1 } else {
callback(null);
}
});
1};
1
MongooseAdapter.prototype.all = function all(model, filter, callback) {
if (!filter) {
filter = {};
2 }
var query = this._models[model].find({});
23 if (filter.where) {
Object.keys(filter.where).forEach(function (k) {
var cond = filter.where[k];
16 var spec = false;
16 if (cond && cond.constructor.name === 'Object') {
spec = Object.keys(cond)[0];
5 cond = cond[spec];
5 }
if (spec) {
if (spec === 'between') {
query.where(k).gte(cond[0]).lte(cond[1]);
1 } else {
query.where(k)[spec](cond);
4 }
} else {
query.where(k, cond);
11 }
});
16 }
if (filter.order) {
var keys = filter.order; // can be Array or String
if (typeof(keys) == "string") {
keys = keys.split(',');
8 }
var args = [];
8
for(index in keys) {
var m = keys[index].match(/\s+(A|DE)SC$/);
10
keys[index] = keys[index].replace(/\s+(A|DE)SC$/, '');
10 if (m && m[1] === 'DE') {
query.sort(keys[index].trim(), -1);
3 } else {
query.sort(keys[index].trim(), 1);
7 }
}
}
if (filter.limit) {
query.limit(filter.limit);
5 }
if (filter.skip) {
query.skip(filter.skip);
} else if (filter.offset) {
query.skip(filter.offset);
}
query.exec(function (err, data) {
if (err) return callback(err);
23 callback(null, data);
23 });
23};
1
MongooseAdapter.prototype.destroyAll = function destroyAll(model, callback) {
var wait = 0;
3 this._models[model].find(function (err, data) {
if (err) return callback(err);
3 wait = data.length;
3 data.forEach(function (obj) {
obj.remove(done)
});
3 });
3
var error = null;
3 function done(err) {
error = error || err;
38 if (--wait === 0) {
callback(error);
3 }
}
};
1
MongooseAdapter.prototype.count = function count(model, callback, where) {
this._models[model].count(where || {}, callback);
3};
1
MongooseAdapter.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
this.getCached(model, id, function (err, inst) {
if (err) {
return cb(err);
} else if (inst) {
merge(inst, data);
2 inst.save(cb);
2 } else cb();
2 });
2};
1
MongooseAdapter.prototype.disconnect = function () {
this.client.connection.close();
1};
1
function merge(base, update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
24 });
5 return base;
5}
/**
* Module dependencies
*/
var AbstractClass = require('./abstract-class').AbstractClass;
1var util = require('util');
1var path = require('path');
1
/**
* Export public API
*/
exports.Schema = Schema;
1// exports.AbstractClass = AbstractClass;
/**
* Helpers
*/
var slice = Array.prototype.slice;
1
/**
* 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;
1 // just save everything we get
this.name = name;
1 this.settings = settings;
1
// Disconnected by default
this.connected = false;
1
// create blank models pool
this.models = {};
1 this.definitions = {};
1
// and initialize schema using adapter
// this is only one initialization entry point of adapter
// this module should define `adapter` member of `this` (schema)
var adapter;
1 if (path.existsSync(__dirname + '/adapters/' + name + '.js')) {
adapter = require('./adapters/' + name);
1 } else {
try {
adapter = require(name);
} catch(e) {
throw new Error('Adapter ' + name + ' is not defined, try\n npm install ' + name);
}
}
adapter.initialize(this, function () {
// we have an adaper now?
if (!this.adapter) {
throw new Error('Adapter is not defined correctly: it should create `adapter` member of schema');
}
this.adapter.log = function (query, start) {
schema.log(query, start);
};
1
this.adapter.logger = function (query) {
var t1 = Date.now();
var log = this.log;
return function (q) {
log(q || query, t1);
};
};
1
this.connected = true;
1 this.emit('connected');
1
}.bind(this));
1};
1
util.inherits(Schema, require('events').EventEmitter);
1
function Text() {}
Schema.Text = Text;
1
/**
* Define 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.define('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;
3 var args = slice.call(arguments);
3
if (!className) throw new Error('Class name required');
3 if (args.length == 1) properties = {}, args.push(properties);
3 if (args.length == 2) settings = {}, args.push(settings);
3
standartize(properties, settings);
3
// every class can receive hash of data as optional param
var newClass = function ModelConstructor(data) {
if (!(this instanceof ModelConstructor)) {
return new ModelConstructor(data);
1 }
AbstractClass.call(this, data);
152 };
3
hiddenProperty(newClass, 'schema', schema);
3 hiddenProperty(newClass, 'modelName', className);
3 hiddenProperty(newClass, 'cache', {});
3 hiddenProperty(newClass, 'mru', []);
3
// setup inheritance
newClass.__proto__ = AbstractClass;
3 util.inherits(newClass, AbstractClass);
3
// store class in model pool
this.models[className] = newClass;
3 this.definitions[className] = {
properties: properties,
settings: settings
};
3
// pass controll to adapter
this.adapter.define({
model: newClass,
properties: properties,
settings: settings
});
3
return newClass;
3
function standartize(properties, settings) {
Object.keys(properties).forEach(function (key) {
var v = properties[key];
14 if (typeof v === 'function') {
properties[key] = { type: v };
7 }
});
3 // TODO: add timestamps fields
// when present in settings: {timestamps: true}
// or {timestamps: {created: 'created_at', updated: false}}
// by default property names: createdAt, updatedAt
}
};
1
/**
* 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);
}
};
1
/**
* 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();
1 if (this.adapter.automigrate) {
this.adapter.automigrate(cb);
} else if (cb) {
cb();
1 }
};
1
/**
* 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();
}
};
1
/**
* 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);
}
};
1
/**
* 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);
};
1
/**
* 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
};
1
/**
* 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;
2
if (this.adapter.defineForeignKey) {
this.adapter.defineForeignKey(className, key, function (err, keyType) {
if (err) throw err;
2 this.definitions[className].properties[key] = {type: keyType};
2 }.bind(this));
2 } else {
this.definitions[className].properties[key] = {type: Number};
}
};
1
/**
* Close database connection
*/
Schema.prototype.disconnect = function disconnect() {
if (typeof this.adapter.disconnect === 'function') {
this.adapter.disconnect();
1 }
};
1
/**
* Define hidden property
*/
function hiddenProperty(where, property, value) {
Object.defineProperty(where, property, {
writable: false,
enumerable: false,
configurable: false,
value: value
});
12}
exports.Hookable = Hookable;
function Hookable() {
// hookable class
};
1
Hookable.afterInitialize = null;
1Hookable.beforeValidation = null;
1Hookable.afterValidation = null;
1Hookable.beforeSave = null;
1Hookable.afterSave = null;
1Hookable.beforeCreate = null;
1Hookable.afterCreate = null;
1Hookable.beforeUpdate = null;
1Hookable.afterUpdate = null;
1Hookable.beforeDestroy = null;
1Hookable.afterDestroy = null;
1
Hookable.prototype.trigger = function trigger(actionName, work, data) {
var capitalizedName = capitalize(actionName);
354 var afterHook = this.constructor["after" + capitalizedName];
354 var beforeHook = this.constructor["before" + capitalizedName];
354 var inst = this;
354
// we only call "before" hook when we have actual action(work) to perform
if (work) {
if (beforeHook) {
// before hook should be called on instance with one param: callback
beforeHook.call(inst, function () {
// actual action also have one param: callback
work.call(inst, next);
}, data);
} else {
work.call(inst, next);
87 }
} else {
next();
267 }
function next(done) {
if (afterHook) {
afterHook.call(inst, done);
} else if (done) {
done.call(this);
87 }
}
};
1
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
354}
exports.inherits = function (newClass, baseClass) {
Object.keys(baseClass).forEach(function (classMethod) {
newClass[classMethod] = baseClass[classMethod];
20 });
2 Object.keys(baseClass.prototype).forEach(function (instanceMethod) {
newClass.prototype[instanceMethod] = baseClass.prototype[instanceMethod];
2 });
2};
1
exports.safeRequire = safeRequire;
function safeRequire(module) {
try {
return require(module);
1 } catch(e) {
console.log('Run "npm install ' + module + '" command to use jugglingdb using this database engine');
process.exit(1);
}
}