/**
* 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);
971}
AbstractClass.prototype._initProperties = function (data, applySetters) {
var self = this;
1659 var ctor = this.constructor;
1659 var ds = ctor.schema.definitions[ctor.modelName];
1659 var properties = ds.properties;
1659 data = data || {};
1659
if (data.id) {
defineReadonlyProp(this, 'id', data.id);
659 }
Object.defineProperty(this, 'cachedRelations', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
1659
Object.keys(properties).forEach(function (attr) {
var _attr = '_' + attr,
attr_was = attr + '_was';
9250
// 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)
))
});
9250
// Public setters and getters
Object.defineProperty(this, attr, {
get: function () {
if (ctor.getter[attr]) {
return ctor.getter[attr].call(this);
} else {
return this[_attr];
4586 }
},
set: function (value) {
if (ctor.setter[attr]) {
ctor.setter[attr].call(this, value);
8 } else {
this[_attr] = value;
105 }
},
configurable: true,
enumerable: true
});
9250
if (data.hasOwnProperty(attr)) {
if (applySetters && ctor.setter[attr]) {
ctor.setter[attr].call(this, data[attr]);
24 }
// Getter for initial property
Object.defineProperty(this, attr_was, {
writable: true,
value: this[_attr],
configurable: true,
enumerable: false
});
3394 }
}.bind(this));
1659
function getDefault(attr) {
var def = properties[attr]['default']
if (isdef(def)) {
if (typeof def === 'function') {
return def();
574 } else {
return def;
659 }
} else {
return null;
3721 }
}
this.trigger("initialize");
1659};
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);
5};
1
AbstractClass.whatTypeName = function (propName) {
var ds = this.schema.definitions[this.modelName];
32 return ds.properties[propName].type.name;
32};
1
AbstractClass.prototype.whatTypeName = function (propName) {
return this.constructor.whatTypeName(propName);
16};
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;
292
var modelName = this.modelName;
292
if (typeof data === 'function') {
callback = data;
41 data = {};
41 }
if (typeof callback !== 'function') {
callback = function () {};
8 }
var obj = null;
292 // if we come from save
if (data instanceof AbstractClass && !data.id) {
obj = data;
44 data = obj.toObject(true);
44 this.prototype._initProperties.call(obj, data, false);
44 create();
44 } else {
obj = new this(data);
248 data = obj.toObject(true);
248
// validation required
obj.isValid(function (valid) {
if (!valid) {
callback(new Error('Validation error'), obj);
1 } else {
create();
247 }
});
248 }
function create() {
obj.trigger('create', function (done) {
this._adapter().create(modelName, data, function (err, id) {
if (id) {
defineReadonlyProp(obj, 'id', id);
290 addToCache(this.constructor, obj);
290 }
done.call(this, function () {
if (callback) {
callback(err, obj);
290 }
});
290 }.bind(this));
290 });
291 }
};
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;
20
var Model = this;
20 if (!data.id) return this.create(data, callback);
12 if (this.schema.adapter.updateOrCreate) {
var inst = new Model(data);
12 this.schema.adapter.updateOrCreate(Model.modelName, inst.toObject(), function (err, data) {
var obj;
12 if (data) {
inst._initProperties(data);
12 obj = inst;
12 } else {
obj = null;
}
if (obj) {
addToCache(Model, obj);
12 }
callback(err, obj);
12 });
12 } 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;
24
if (id) {
this.schema.adapter.exists(this.modelName, id, cb);
24 } 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;
68
this.schema.adapter.find(this.modelName, id, function (err, data) {
var obj = null;
68 if (data) {
var cached = getCached(this, data.id);
60 if (cached) {
obj = cached;
substractDirtyAttributes(obj, data);
// maybe just obj._initProperties(data); instead of
this.prototype._initProperties.call(obj, data);
} else {
data.id = id;
60 obj = new this();
60 obj._initProperties(data, false);
60 addToCache(this, id);
60 }
}
cb(err, obj);
68 }.bind(this));
68};
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;
162
if (arguments.length === 1) {
cb = params;
16 params = null;
16 }
var constr = this;
162 this.schema.adapter.all(this.modelName, params, function (err, data) {
var collection = null;
162 if (data && data.map) {
collection = data.map(function (d) {
var obj = null;
530 // do not create different instances for the same object
var cached = getCached(constr, d.id);
530 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;
530 obj._initProperties(d, false);
530 if (obj.id) addToCache(constr, obj);
530 }
return obj;
530 });
162 cb(err, collection);
162 }
});
162};
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;
24
if (typeof params === 'function') {
cb = params;
8 params = {};
8 }
params.limit = 1;
24 this.all(params, function (err, collection) {
if (err || !collection || !collection.length > 0) return cb(err);
16 cb(err, collection[0]);
16 });
24};
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;
21
this.schema.adapter.destroyAll(this.modelName, function (err) {
clearCache(this);
21 cb(err);
21 }.bind(this));
21};
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;
24
if (typeof where === 'function') {
cb = where;
16 where = null;
16 }
this.schema.adapter.count(this.modelName, cb, where);
24};
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;
69
if (typeof options == 'function') {
callback = options;
67 options = {};
67 }
callback = callback || function () {};
69 options = options || {};
69
if (!('validate' in options)) {
options.validate = true;
68 }
if (!('throws' in options)) {
options.throws = false;
68 }
if (options.validate) {
this.isValid(function (valid) {
if (valid) {
save.call(this);
67 } else {
var err = new Error('Validation error');
1 // throws option is dangerous for async usage
if (options.throws) {
throw err;
1 }
callback(err, this);
}
}.bind(this));
67 } else {
save.call(this);
1 }
function save() {
this.trigger('save', function (saveDone) {
var modelName = this.constructor.modelName;
68 var data = this.toObject(true);
68 var inst = this;
68 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);
24 }
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
24 });
24 });
24 });
24 }, data);
24 } else {
inst.constructor.create(inst, function (err) {
saveDone.call(inst, function () {
callback(err, inst);
44 });
44 });
44 }
});
68 }
};
1
AbstractClass.prototype.isNewRecord = function () {
return !this.id;
28};
1
/**
* Return adapter of current record
* @private
*/
AbstractClass.prototype._adapter = function () {
return this.constructor.schema.adapter;
341};
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 = {};
406 var ds = this.constructor.schema.definitions[this.constructor.modelName];
406 var properties = ds.properties;
406 // weird
Object.keys(onlySchema ? properties : this).concat(['id']).forEach(function (property) {
data[property] = this[property];
2767 }.bind(this));
406 return data;
406};
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;
10
this.trigger('destroy', function (destroyed) {
this._adapter().destroy(this.constructor.modelName, this.id, function (err) {
removeFromCache(this.constructor, this.id);
9 destroyed(function () {
cb && cb(err);
8 });
9 }.bind(this));
9 });
10};
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 = {};
8 data[name] = value;
8 this.updateAttributes(data, callback);
8};
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;
20
var inst = this;
20 var model = this.constructor.modelName;
20
// update instance's properties
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
29 });
20
inst.isValid(function (valid) {
if (!valid) {
if (cb) {
cb(new Error('Validation error'));
1 }
} else {
update();
19 }
});
20
function update() {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (done) {
Object.keys(data).forEach(function (key) {
data[key] = inst[key];
27 });
18
inst._adapter().updateAttributes(model, inst.id, data, function (err) {
if (!err) {
inst._initProperties(data, false);
18 /*
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);
17 });
17 });
18 });
18 }, data);
19 });
19 }
};
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'];
89};
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;
16
var obj = getCached(this.constructor, this.id);
16 if (obj) obj.reset();
16 this.constructor.find(this.id, callback);
16};
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;
8 // 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 = {};
64 x[fk] = this.id;
64 return {where: x};
64 }, {
find: find,
destroy: destroy
});
8
// obviously, anotherClass should have attribute called `fk`
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
8
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;
16 var fk = params.foreignKey;
16
this.schema.defineForeignKey(this.modelName, fk);
16 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];
16 } else { // setter
this[fk] = p;
8 }
};
16
};
1
/**
* Define scope
* TODO: describe behavior and usage examples
*/
AbstractClass.scope = function (name, params) {
defineScope(this, this, name, params);
8};
1
function defineScope(cls, targetClass, name, params, methods) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
8 }
// anly make sence to add scope in meta if base and target classes
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
8 } else {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
8 }
}
Object.defineProperty(cls, name, {
enumerable: false,
configurable: true,
get: function () {
var f = function caller(cond, cb) {
var actualCond;
8 if (arguments.length === 1) {
actualCond = {};
8 cb = cond;
8 } 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);
8 };
96 f._scope = typeof params === 'function' ? params.call(this) : params;
96 f.build = build;
96 f.create = create;
96 f.destroyAll = destroyAll;
96 for (var i in methods) {
f[i] = methods[i].bind(this);
128 }
// define sub-scopes
Object.keys(targetClass._scopeMeta).forEach(function (name) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
mergeParams(f._scope, targetClass._scopeMeta[name]);
24 return f;
24 }
});
56 }.bind(this));
96 return f;
96 }
});
16
// and it should have create/build methods with binded thisModelNameId param
function build(data) {
data = data || {};
24 return new targetClass(mergeParams(this._scope, {where:data}).where);
24 }
function create(data, cb) {
if (typeof data === 'function') {
cb = data;
16 data = {};
16 }
this.build(data).save(cb);
16 }
function destroyAll(id, cb) {
// implement me
}
function mergeParams(base, update) {
if (update.where) {
base.where = merge(base.where, update.where);
56 }
// overwrite order
if (update.order) {
base.order = update.order;
}
return base;
56
}
}
/**
* Check whether `s` is not undefined
* @param {Mixed} s
* @return {Boolean} s is undefined
*/
function isdef(s) {
var undef;
19675 return s !== undef;
19675}
/**
* 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 || {};
56 if (update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
32 });
56 }
return base;
56}
/**
* 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
});
949}
/**
* Add object to cache
*/
function addToCache(constr, obj) {
return;
892 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;
600
var ind = constr.mru.indexOf(id);
600 if (~ind) constr.mru.splice(ind, 1);
600 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);
606 return id && constr.cache[id];
606}
/**
* Clear cache (fully)
*
* removes both cache and LRU index
*
* @param {Class} constr - class constructor
*/
function clearCache(constr) {
constr.cache = {};
21 constr.mru = [];
21}
/**
* Remove object from cache
*
* @param {Class} constr
* @param {id} id
*/
function removeFromCache(constr, id) {
var ind = constr.mru.indexOf(id);
9 if (!~ind) constr.mru.splice(ind, 1);
9 delete constr.cache[id];
9}
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var redis = safeRequire('redis');
1
exports.initialize = function initializeSchema(schema, callback) {
if (!redis) return;
1
if (schema.settings.url) {
var url = require('url');
var redisUrl = url.parse(schema.settings.url);
var redisAuth = (redisUrl.auth || '').split(':');
schema.settings.host = redisUrl.hostname;
schema.settings.port = redisUrl.port;
if (redisAuth.length == 2) {
schema.settings.db = redisAuth[0];
schema.settings.password = redisAuth[1];
}
}
schema.client = redis.createClient(
schema.settings.port,
schema.settings.host,
schema.settings.options
);
1 schema.client.auth(schema.settings.password);
1 schema.client.on('connect', callback);
1
schema.adapter = new BridgeToRedis(schema.client);
1};
1
function BridgeToRedis(client) {
this._models = {};
1 this.client = client;
1 this.indexes = {};
1}
BridgeToRedis.prototype.define = function (descr) {
var m = descr.model.modelName;
3 this._models[m] = descr;
3 this.indexes[m] = {};
3 Object.keys(descr.properties).forEach(function (prop) {
if (descr.properties[prop].index) {
this.indexes[m][prop] = descr.properties[prop].type;
4 }
}.bind(this));
3};
1
BridgeToRedis.prototype.defineForeignKey = function (model, key, cb) {
this.indexes[model][key] = Number;
2 cb(null, Number);
2};
1
BridgeToRedis.prototype.save = function (model, data, callback) {
deleteNulls(data);
34 var log = this.logger('HMSET ' + model + ':' + data.id + ' ...');
34 this.client.hmset(model + ':' + data.id, data, function (err) {
log();
34 if (err) return callback(err);
34 this.updateIndexes(model, data.id, data, callback);
34 }.bind(this));
34};
1
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback) {
var i = this.indexes[model];
36 var schedule = [['sadd', 's:' + model, id]];
36 Object.keys(data).forEach(function (key) {
if (i[key]) {
schedule.push([
'sadd',
'i:' + model + ':' + key + ':' + data[key],
model + ':' + id
]);
56 }
}.bind(this));
36
if (schedule.length) {
this.client.multi(schedule).exec(function (err) {
callback(err, data);
36 });
36 } else {
callback(null);
}
};
1
BridgeToRedis.prototype.create = function (model, data, callback) {
if (data.id) return create.call(this, data.id, true);
29
var log = this.logger('INCR id:' + model);
29 this.client.incr('id:' + model, function (err, id) {
log();
29 create.call(this, id);
29 }.bind(this));
29
function create(id, upsert) {
data.id = id;
29 this.save(model, data, function (err) {
if (callback) {
callback(err, id);
29 }
});
29
// push the id to the list of user ids for sorting
log('SADD s:' + model + ' ' + data.id);
29 this.client.sadd("s:" + model, upsert ? data : data.id);
29 }
};
1
BridgeToRedis.prototype.updateOrCreate = function (model, data, callback) {
if (!data.id) return this.create(model, data, callback);
2 this.save(model, data, callback);
2};
1
BridgeToRedis.prototype.exists = function (model, id, callback) {
var log = this.logger('EXISTS ' + model + ':' + id);
3 this.client.exists(model + ':' + id, function (err, exists) {
log();
3 if (callback) {
callback(err, exists);
3 }
});
3};
1
BridgeToRedis.prototype.find = function find(model, id, callback) {
var t1 = Date.now();
9 this.client.hgetall(model + ':' + id, function (err, data) {
this.log('HGETALL ' + model + ':' + id, t1);
9 if (data && Object.keys(data).length > 0) {
data.id = id;
8 } else {
data = null;
1 }
callback(err, data);
9 }.bind(this));
9};
1
BridgeToRedis.prototype.destroy = function destroy(model, id, callback) {
var t1 = Date.now();
1 this.client.del(model + ':' + id, function (err) {
this.log('DEL ' + model + ':' + id, t1);
1 callback(err);
1 }.bind(this));
1 this.log('SREM s:' + model, t1);
1 this.client.srem("s:" + model, id);
1};
1
BridgeToRedis.prototype.possibleIndexes = function (model, filter) {
if (!filter || Object.keys(filter.where || {}).length === 0) return false;
13
var foundIndex = [];
13 var noIndex = [];
13 Object.keys(filter.where).forEach(function (key) {
if (this.indexes[model][key] && (typeof filter.where[key] === 'string' || typeof filter.where[key] === 'number')) {
foundIndex.push('i:' + model + ':' + key + ':' + filter.where[key]);
10 } else {
noIndex.push(key);
3 }
}.bind(this));
13
return [foundIndex, noIndex];
13};
1
BridgeToRedis.prototype.all = function all(model, filter, callback) {
var ts = Date.now();
18 var client = this.client;
18 var log = this.log;
18 var t1 = Date.now();
18 var cmd;
18 var that = this;
18 var sortCmd = [];
18 var props = this._models[model].properties;
18 var allNumeric = true;
18
// TODO: we need strict mode when filtration only possible when we have indexes
// WHERE
if (filter && filter.where) {
var pi = this.possibleIndexes(model, filter);
13 var indexes = pi[0];
13 var noIndexes = pi[1];
13
if (indexes && indexes.length) {
cmd = 'SINTER "' + indexes.join('" "') + '"';
10 if (noIndexes.length) {
log(model + ': no indexes found for ', noIndexes.join(', '),
'slow sorting and filtering');
}
indexes.push(noIndexes.length ? orderLimitStageBad : orderLimitStage);
10 client.sinter.apply(client, indexes);
10 } else {
// filter manually
cmd = 'KEYS ' + model + ':*';
3 client.keys(model + ':*', orderLimitStageBad);
3 }
} else {
// no filtering, just sort/limit (if any)
gotKeys('*');
5 }
// bad case when we trying to filter on non-indexed fields
// in bad case we need retrieve all data and filter/limit/sort manually
function orderLimitStageBad(err, keys) {
log(cmd, t1);
3 var t2 = Date.now();
3 if (err) {
return callback(err, []);
}
var query = keys.map(function (key) {
return ['hgetall', key];
40 });
3 client.multi(query).exec(function (err, replies) {
log(query, t2);
3 gotFilteredData(err, replies.filter(applyFilter(filter)));
3 });
3
function gotFilteredData(err, nodes) {
if (err) return callback(null);
3
if (filter.order) {
var allNumeric = true;
var orders = filter.order;
if (typeof filter.order === "string") {
orders = [filter.order];
}
orders.forEach(function (key) {
key = key.split(' ')[0];
if (props[key].type.name !== 'Number' && props[key].type.name !== 'Date') {
allNumeric = false;
}
});
if (allNumeric) {
nodes = nodes.sort(numerically.bind(orders));
} else {
nodes = nodes.sort(literally.bind(orders));
}
}
// LIMIT
if (filter && filter.limit) {
var from = (filter.offset || 0), to = from + filter.limit;
callback(null, nodes.slice(from, to));
} else {
callback(null, nodes);
3 }
}
}
function orderLimitStage(err, keys) {
log(cmd, t1);
10 var t2 = Date.now();
10 if (err) {
return callback(err, []);
}
gotKeys(keys);
10 }
function gotKeys(keys) {
// ORDER
var reverse = false;
15 if (filter && filter.order) {
var orders = filter.order;
6 if (typeof filter.order === "string"){
orders = [filter.order];
6 }
orders.forEach(function (key) {
var m = key.match(/\s+(A|DE)SC$/i);
6 if (m) {
key = key.replace(/\s+(A|DE)SC/i, '');
3 if (m[1] === 'DE') reverse = true;
3 }
if (key !== 'id') {
if (props[key].type.name !== 'Number' && props[key].type.name !== 'Date') {
allNumeric = false;
5 }
}
sortCmd.push("BY", model + ":*->" + key);
6 });
6 }
// LIMIT
if (keys === '*' && filter && filter.limit){
var from = (filter.offset || 0), to = from + filter.limit;
1 sortCmd.push("LIMIT", from, to);
1 }
// we need ALPHA modifier when sorting string values
// the only case it's not required - we sort numbers
// TODO: check if we sort numbers
if (!allNumeric) {
sortCmd.push('ALPHA');
5 }
if (reverse) {
sortCmd.push('DESC');
2 }
if (sortCmd.length) {
sortCmd.unshift("s:" + model);
7 sortCmd.push("GET", "#");
7 cmd = "SORT " + sortCmd.join(" ");
7 var ttt = Date.now();
7 sortCmd.push(function(err, ids){
if (err) {
return callback(err, []);
}
log(cmd, ttt);
7 var sortedKeys = ids.map(function (i) {
return model + ":" + i;
35 });
7 handleKeys(err, intersect(sortedKeys, keys));
7 });
7 client.sort.apply(client, sortCmd);
7 } else {
// no sorting or filtering: just get all keys
if (keys === '*') {
cmd = 'KEYS ' + model + ':*';
2 client.keys(model + ':*', handleKeys);
2 } else {
handleKeys(null, keys);
6 }
}
}
function handleKeys(err, keys) {
var t2 = Date.now();
15 var query = keys.map(function (key) {
return ['hgetall', key];
96 });
15 client.multi(query).exec(function (err, replies) {
log(query, t2);
15 // console.log('Redis time: %dms', Date.now() - ts);
callback(err, filter ? replies.filter(applyFilter(filter)) : replies);
15 });
15 }
return;
18
function numerically(a, b) {
return a[this[0]] - b[this[0]];
}
function literally(a, b) {
return a[this[0]] > b[this[0]];
}
// TODO: find better intersection method
function intersect(sortedKeys, filteredKeys) {
if (filteredKeys === '*') return sortedKeys;
4 var index = {};
4 filteredKeys.forEach(function (x) {
index[x] = true;
288 });
4 return sortedKeys.filter(function (x) {
4 return index[x];
24 });
}
};
1
function applyFilter(filter) {
if (typeof filter.where === 'function') {
return filter.where;
}
var keys = Object.keys(filter.where || {});
16 return function (obj) {
16 var pass = true;
120 keys.forEach(function (key) {
if (!test(filter.where[key], obj[key])) {
pass = false;
84 }
});
120 return pass;
120 };
function test(example, value) {
if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
return value.match(example);
15 }
// not strict equality
return example == value;
94 }
}
BridgeToRedis.prototype.destroyAll = function destroyAll(model, callback) {
var keysQuery = model + ':*';
2 var t1 = Date.now();
2 this.client.keys(keysQuery, function (err, keys) {
this.log('KEYS ' + keysQuery, t1);
2 if (err) {
return callback(err, []);
}
var query = keys.map(function (key) {
return ['del', key];
30 });
2 var t2 = Date.now();
2 this.client.multi(query).exec(function (err, replies) {
this.log(query, t2);
2 this.client.del('s:' + model, function () {
callback(err);
2 });
2 }.bind(this));
2 }.bind(this));
2};
1
BridgeToRedis.prototype.count = function count(model, callback, where) {
var keysQuery = model + ':*';
3 var t1 = Date.now();
3 if (where && Object.keys(where).length) {
this.all(model, {where: where}, function (err, data) {
callback(err, err ? null : data.length);
1 });
1 } else {
this.client.keys(keysQuery, function (err, keys) {
this.log('KEYS ' + keysQuery, t1);
2 callback(err, err ? null : keys.length);
2 }.bind(this));
2 }
};
1
BridgeToRedis.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
var t1 = Date.now();
2 deleteNulls(data);
2 this.client.hmset(model + ':' + id, data, function () {
this.log('HMSET ' + model + ':' + id, t1);
2 this.updateIndexes(model, id, data, cb);
2 }.bind(this));
2};
1
function deleteNulls(data) {
Object.keys(data).forEach(function (key) {
if (data[key] === null) delete data[key];
231 });
36}
BridgeToRedis.prototype.disconnect = function disconnect() {
this.log('QUIT', Date.now());
1 this.client.quit();
1};
1
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var mysql = safeRequire('mysql');
1var BaseSQL = require('../sql');
1
exports.initialize = function initializeSchema(schema, callback) {
if (!mysql) return;
2
var s = schema.settings;
2 schema.client = mysql.createClient({
host: s.host || 'localhost',
port: s.port || 3306,
user: s.username,
password: s.password,
debug: s.debug
});
2
schema.adapter = new MySQL(schema.client);
2 schema.adapter.schema = schema;
2 // schema.client.query('SET TIME_ZONE = "+04:00"', callback);
schema.client.query('USE ' + s.database, function (err) {
if (err && err.message.match(/^unknown database/i)) {
var dbName = s.database;
schema.client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
schema.client.query('USE ' + s.database, callback);
} else {
throw error;
}
});
} else callback();
2 });
2};
1
/**
* MySQL adapter
*/
function MySQL(client) {
this._models = {};
2 this.client = client;
2}
require('util').inherits(MySQL, BaseSQL);
1
MySQL.prototype.query = function (sql, callback) {
if (!this.schema.connected) {
return this.schema.on('connected', function () {
1 this.query(sql, callback);
1 }.bind(this));
}
var client = this.client;
105 var time = Date.now();
105 var log = this.log;
105 if (typeof callback !== 'function') throw new Error('callback should be a function');
105 this.client.query(sql, function (err, data) {
if (err && err.message.match(/^unknown database/i)) {
var dbName = err.message.match(/^unknown database ~~~S11~~~/i)[1];
client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
client.query(sql, callback);
} else {
callback(err);
}
});
return;
}
if (log) log(sql, time);
105 callback(err, data);
105 });
105};
1
/**
* Must invoke callback(err, id)
*/
MySQL.prototype.create = function (model, data, callback) {
var fields = this.toFields(model, data);
39 var sql = 'INSERT INTO ' + this.tableEscaped(model);
39 if (fields) {
sql += ' SET ' + fields;
39 } else {
sql += ' VALUES ()';
}
this.query(sql, function (err, info) {
callback(err, info && info.insertId);
39 });
39};
1
MySQL.prototype.updateOrCreate = function (model, data, callback) {
var mysql = this;
2 var fieldsNames = [];
2 var fieldValues = [];
2 var combined = [];
2 var props = this._models[model].properties;
2 Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '`' + key + '`';
12 var v;
12 if (key !== 'id') {
v = mysql.toDatabase(props[key], data[key]);
10 } else {
v = data[key];
2 }
fieldsNames.push(k);
12 fieldValues.push(v);
12 if (key !== 'id') combined.push(k + ' = ' + v);
12 }
});
2
var sql = 'INSERT INTO ' + this.tableEscaped(model);
2 sql += ' (' + fieldsNames.join(', ') + ')';
2 sql += ' VALUES (' + fieldValues.join(', ') + ')';
2 sql += ' ON DUPLICATE KEY UPDATE ' + combined.join(', ');
2
this.query(sql, function (err, info) {
if (!err && info && info.insertId) {
data.id = info.insertId;
2 }
callback(err, data);
2 });
2};
1
MySQL.prototype.toFields = function (model, data) {
var fields = [];
44 var props = this._models[model].properties;
44 Object.keys(data).forEach(function (key) {
if (props[key]) {
fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + this.toDatabase(props[key], data[key]));
239 }
}.bind(this));
44 return fields.join(',');
44};
1
function dateToMysql(val) {
return val.getUTCFullYear() + '-' +
39 fillZeros(val.getUTCMonth() + 1) + '-' +
fillZeros(val.getUTCDate()) + ' ' +
fillZeros(val.getUTCHours()) + ':' +
fillZeros(val.getUTCMinutes()) + ':' +
fillZeros(val.getUTCSeconds());
function fillZeros(v) {
return v < 10 ? '0' + v : v;
195 }
}
MySQL.prototype.toDatabase = function (prop, val) {
if (val === null) return 'NULL';
121 if (val.constructor.name === 'Object') {
var operator = Object.keys(val)[0]
val = val[operator];
5 if (operator === 'between') {
return this.toDatabase(prop, val[0]) +
1 ' AND ' +
this.toDatabase(prop, val[1]);
} else if (operator == 'inq' || operator == 'nin') {
if (!(val.propertyIsEnumerable('length')) && typeof val === 'object' && typeof val.length === 'number') { //if value is array
return val.join(',');
} else {
return val;
}
}
}
if (!prop) return val;
120 if (prop.type.name === 'Number') return val;
118 if (prop.type.name === 'Date') {
if (!val) return 'NULL';
39 if (!val.toUTCString) {
val = new Date(val);
14 }
return '"' + dateToMysql(val) + '"';
39 }
if (prop.type.name == "Boolean") return val ? 1 : 0;
47 return this.client.escape(val.toString());
47};
1
MySQL.prototype.fromDatabase = function (model, data) {
if (!data) return null;
74 var props = this._models[model].properties;
74 Object.keys(data).forEach(function (key) {
var val = data[key];
478 if (props[key]) {
if (props[key].type.name === 'Date' && val !== null) {
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
57 }
}
data[key] = val;
478 });
74 return data;
74};
1
MySQL.prototype.escapeName = function (name) {
return '`' + name.replace(/\./g, '`.`') + '`';
114};
1
MySQL.prototype.all = function all(model, filter, callback) {
var sql = 'SELECT * FROM ' + this.tableEscaped(model);
21 var self = this;
21 var props = this._models[model].properties;
21
if (filter) {
if (filter.where) {
sql += ' ' + buildWhere(filter.where);
16 }
if (filter.order) {
sql += ' ' + buildOrderBy(filter.order);
6 }
if (filter.limit) {
sql += ' ' + buildLimit(filter.limit, filter.offset || 0);
5 }
}
this.query(sql, function (err, data) {
if (err) {
return callback(err, []);
}
callback(null, data.map(function (obj) {
return self.fromDatabase(model, obj);
66 }));
21 }.bind(this));
21
return sql;
21
function buildWhere(conds) {
var cs = [];
16 Object.keys(conds).forEach(function (key) {
var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`'
var val = self.toDatabase(props[key], conds[key]);
16 if (conds[key] === null) {
cs.push(keyEscaped + ' IS NULL');
1 } else if (conds[key].constructor.name === 'Object') {
var condType = Object.keys(conds[key])[0];
5 var sqlCond = keyEscaped;
5 switch (condType) {
case 'gt':
sqlCond += ' > ';
1 break;
1 case 'gte':
sqlCond += ' >= ';
1 break;
1 case 'lt':
sqlCond += ' < ';
1 break;
1 case 'lte':
sqlCond += ' <= ';
1 break;
1 case 'between':
sqlCond += ' BETWEEN ';
1 break;
1 case 'inq':
sqlCond += ' IN ';
break;
case 'nin':
sqlCond += ' NOT IN ';
break;
case 'neq':
sqlCond + ' != ';
break;
}
sqlCond += (condType == 'inq' || condType == 'nin') ? '(' + val + ')' : val;
5 cs.push(sqlCond);
5 } else {
cs.push(keyEscaped + ' = ' + val);
10 }
});
16 if (cs.length === 0) {
return '';
}
return 'WHERE ' + cs.join(' AND ');
16 }
function buildOrderBy(order) {
if (typeof order === 'string') order = [order];
6 return 'ORDER BY ' + order.join(', ');
6 }
function buildLimit(limit, offset) {
return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit);
5 }
};
1
MySQL.prototype.autoupdate = function (cb) {
var self = this;
1 var wait = 0;
1 Object.keys(this._models).forEach(function (model) {
wait += 1;
1 self.query('SHOW FIELDS FROM ' + self.tableEscaped(model), function (err, fields) {
if (!err && fields.length) {
self.alterTable(model, fields, done);
1 } else {
self.createTable(model, done);
}
});
1 });
1
function done(err) {
if (err) {
console.log(err);
}
if (--wait === 0 && cb) {
cb();
1 }
}
};
1
MySQL.prototype.isActual = function (cb) {
var ok = false;
2 var self = this;
2 var wait = 0;
2 Object.keys(this._models).forEach(function (model) {
wait += 1;
2 self.query('SHOW FIELDS FROM ' + model, function (err, fields) {
self.alterTable(model, fields, done, true);
2 });
2 });
2
function done(err, needAlter) {
if (err) {
console.log(err);
}
ok = ok || needAlter;
2 if (--wait === 0 && cb) {
cb(null, !ok);
2 }
}
};
1
MySQL.prototype.alterTable = function (model, actualFields, done, checkOnly) {
var self = this;
3 var m = this._models[model];
3 var propNames = Object.keys(m.properties).filter(function (name) {
return !!m.properties[name];
24 });
3 var sql = [];
3
// change/add new fields
propNames.forEach(function (propName) {
var found;
20 actualFields.forEach(function (f) {
if (f.Field === propName) {
found = f;
19 }
});
20
if (found) {
actualize(propName, found);
19 } else {
sql.push('ADD COLUMN `' + propName + '` ' + self.propertySettingsSQL(model, propName));
1 }
});
3
// drop columns
actualFields.forEach(function (f) {
var notFound = !~propNames.indexOf(f.Field);
24 if (f.Field === 'id') return;
21 if (notFound || !m.properties[f.Field]) {
sql.push('DROP COLUMN `' + f.Field + '`');
2 }
});
3
if (sql.length) {
if (checkOnly) {
done(null, true);
1 } else {
this.query('ALTER TABLE `' + model + '` ' + sql.join(',\n'), done);
1 }
} else {
done();
1 }
function actualize(propName, oldSettings) {
var newSettings = m.properties[propName];
19 if (newSettings && changed(newSettings, oldSettings)) {
sql.push('CHANGE COLUMN `' + propName + '` `' + propName + '` ' + self.propertySettingsSQL(model, propName));
2 }
}
function changed(newSettings, oldSettings) {
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true;
19 if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true;
18 if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true;
17 return false;
17 }
};
1
MySQL.prototype.propertiesSQL = function (model) {
var self = this;
4 var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY'];
4 Object.keys(this._models[model].properties).forEach(function (prop) {
sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
21 });
4 return sql.join(',\n ');
4
};
1
MySQL.prototype.propertySettingsSQL = function (model, prop) {
var p = this._models[model].properties[prop];
24 return datatype(p) + ' ' +
24 (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
};
1
function datatype(p) {
var dt = '';
42 switch (p.type.name) {
case 'String':
dt = 'VARCHAR(' + (p.limit || 255) + ')';
17 break;
17 case 'Text':
dt = 'TEXT';
6 break;
6 case 'Number':
dt = 'INT(' + (p.limit || 11) + ')';
7 break;
7 case 'Date':
dt = 'DATETIME';
6 break;
6 case 'Boolean':
dt = 'TINYINT(1)';
6 break;
6 }
return dt;
42}
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var sqlite3 = safeRequire('sqlite3');
1var BaseSQL = require('../sql');
1
exports.initialize = function initializeSchema(schema, callback) {
if (!sqlite3) return;
1 var s = schema.settings;
1 var Database = sqlite3.verbose().Database;
1 var db = new Database(s.database);
1
schema.client = db;
1
schema.adapter = new SQLite3(schema.client);
1 if (s.database === ':memory:') {
schema.adapter.automigrate(callback);
1 } else {
process.nextTick(callback);
}
};
1
function SQLite3(client) {
this._models = {};
1 this.client = client;
1}
require('util').inherits(SQLite3, BaseSQL);
1
SQLite3.prototype.command = function () {
this.query('run', [].slice.call(arguments));
55};
1
SQLite3.prototype.queryAll = function () {
this.query('all', [].slice.call(arguments));
21};
1
SQLite3.prototype.queryOne = function () {
this.query('get', [].slice.call(arguments));
15};
1
SQLite3.prototype.query = function (method, args) {
var time = Date.now();
91 var log = this.log;
91 var cb = args.pop();
91 if (typeof cb === 'function') {
args.push(function (err, data) {
if (log) log(args[0], time);
91 cb.call(this, err, data);
91 });
91 } else {
args.push(cb);
args.push(function (err, data) {
log(args[0], time);
});
}
this.client[method].apply(this.client, args);
91};
1
SQLite3.prototype.save = function (model, data, callback) {
var queryParams = [];
5 var sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' +
Object.keys(data).map(function (key) {
queryParams.push(data[key]);
23 return key + ' = ?';
23 }).join(', ') + ' WHERE id = ' + data.id;
5
this.command(sql, queryParams, function (err) {
callback(err);
5 });
5};
1
/**
* Must invoke callback(err, id)
*/
SQLite3.prototype.create = function (model, data, callback) {
data = data || {};
38 var questions = [];
38 var values = Object.keys(data).map(function (key) {
questions.push('?');
252 return data[key];
252 });
38 var sql = 'INSERT INTO ' + this.tableEscaped(model) + ' (' + Object.keys(data).join(',') + ') VALUES ('
sql += questions.join(',');
38 sql += ')';
38 this.command(sql, values, function (err) {
callback(err, this && this.lastID);
38 });
38};
1
SQLite3.prototype.updateOrCreate = function (model, data, callback) {
data = data || {};
2 var questions = [];
2 var values = Object.keys(data).map(function (key) {
questions.push('?');
12 return data[key];
12 });
2 var sql = 'INSERT OR REPLACE INTO ' + this.tableEscaped(model) + ' (' + Object.keys(data).join(',') + ') VALUES ('
sql += questions.join(',');
2 sql += ')';
2 this.command(sql, values, function (err) {
if (!err && this) {
data.id = this.lastID;
2 }
callback(err, data);
2 });
2};
1
SQLite3.prototype.toFields = function (model, data) {
var fields = [];
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key]) {
fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + this.toDatabase(props[key], data[key]));
}
}.bind(this));
return fields.join(',');
};
1
function dateToMysql(val) {
return val.getUTCFullYear() + '-' +
fillZeros(val.getUTCMonth() + 1) + '-' +
fillZeros(val.getUTCDate()) + ' ' +
fillZeros(val.getUTCHours()) + ':' +
fillZeros(val.getUTCMinutes()) + ':' +
fillZeros(val.getUTCSeconds());
function fillZeros(v) {
return v < 10 ? '0' + v : v;
}
}
SQLite3.prototype.toDatabase = function (prop, val) {
if (!prop) return val;
11 if (prop.type.name === 'Number') return val;
10 if (val === null) return 'NULL';
10 if (prop.type.name === 'Date') {
if (!val) return 'NULL';
2 if (!val.toUTCString) {
val = new Date(val);
}
return val;
2 }
if (prop.type.name == "Boolean") return val ? 1 : 0;
8 return val.toString();
8};
1
SQLite3.prototype.fromDatabase = function (model, data) {
if (!data) return null;
74 var props = this._models[model].properties;
74 Object.keys(data).forEach(function (key) {
var val = data[key];
478 if (props[key]) {
if (props[key].type.name === 'Date') {
val = new Date(parseInt(val));
74 }
}
data[key] = val;
478 });
74 return data;
74};
1
SQLite3.prototype.escapeName = function (name) {
return '`' + name + '`';
92};
1
SQLite3.prototype.exists = function (model, id, callback) {
var sql = 'SELECT 1 FROM ' + this.tableEscaped(model) + ' WHERE id = ' + id + ' LIMIT 1';
3 this.queryOne(sql, function (err, data) {
if (err) return callback(err);
3 callback(null, data && data['1'] === 1);
3 });
3};
1
SQLite3.prototype.find = function find(model, id, callback) {
var sql = 'SELECT * FROM ' + this.tableEscaped(model) + ' WHERE id = ' + id + ' LIMIT 1';
9 this.queryOne(sql, function (err, data) {
if (data) {
data.id = id;
8 } else {
data = null;
1 }
callback(err, this.fromDatabase(model, data));
9 }.bind(this));
9};
1
SQLite3.prototype.all = function all(model, filter, callback) {
var sql = 'SELECT * FROM ' + this.tableEscaped(model);
21 var self = this;
21 var props = this._models[model].properties;
21 var queryParams = [];
21
if (filter) {
if (filter.where) {
sql += ' ' + buildWhere(filter.where);
16 }
if (filter.order) {
sql += ' ' + buildOrderBy(filter.order);
6 }
if (filter.limit) {
sql += ' ' + buildLimit(filter.limit, filter.offset || 0);
5 }
}
this.queryAll(sql, queryParams, function (err, data) {
if (err) {
return callback(err, []);
}
callback(null, data.map(function (obj) {
return self.fromDatabase(model, obj);
66 }));
21 }.bind(this));
21
return sql;
21
function buildWhere(conds) {
var cs = [];
16 Object.keys(conds).forEach(function (key) {
var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`'
if (conds[key] === null) {
cs.push(keyEscaped + ' IS NULL');
1 } else if (conds[key].constructor.name === 'Object') {
var condType = Object.keys(conds[key])[0];
5 var sqlCond = keyEscaped;
5 switch (condType) {
case 'gt':
sqlCond += ' > ';
1 break;
1 case 'gte':
sqlCond += ' >= ';
1 break;
1 case 'lt':
sqlCond += ' < ';
1 break;
1 case 'lte':
sqlCond += ' <= ';
1 break;
1 case 'between':
sqlCond += ' BETWEEN ? AND ?';
1 queryParams.push(conds[key][condType][0]);
1 queryParams.push(conds[key][condType][1]);
1 break;
1 }
if (condType !== 'between') {
sqlCond += '?';
4 queryParams.push(conds[key][condType]);
4 }
cs.push(sqlCond);
5 } else {
cs.push(keyEscaped + ' = ?');
10 queryParams.push(self.toDatabase(props[key], conds[key]));
10 }
});
16 return 'WHERE ' + cs.join(' AND ');
16 }
function buildOrderBy(order) {
if (typeof order === 'string') order = [order];
6 return 'ORDER BY ' + order.join(', ');
6 }
function buildLimit(limit, offset) {
return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit);
5 }
};
1
SQLite3.prototype.count = function count(model, callback, where) {
var self = this;
3 var props = this._models[model].properties;
3 var queryParams = [];
3
this.queryOne('SELECT count(*) as cnt FROM ' +
this.tableEscaped(model) + ' ' + buildWhere(where), queryParams, function (err, res) {
if (err) return callback(err);
3 callback(err, err ? null : res.cnt);
3 });
3
function buildWhere(conds) {
var cs = [];
3 Object.keys(conds || {}).forEach(function (key) {
var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`'
if (conds[key] === null) {
cs.push(keyEscaped + ' IS NULL');
} else {
cs.push(keyEscaped + ' = ?');
1 queryParams.push(self.toDatabase(props[key], conds[key]));
1 }
});
3 return cs.length ? ' WHERE ' + cs.join(' AND ') : '';
3 }
};
1
SQLite3.prototype.disconnect = function disconnect() {
this.client.close();
1};
1
SQLite3.prototype.autoupdate = function (cb) {
var self = this;
var wait = 0;
Object.keys(this._models).forEach(function (model) {
wait += 1;
self.queryAll('SHOW FIELDS FROM ' + this.tableEscaped(model), function (err, fields) {
self.alterTable(model, fields, done);
});
});
function done(err) {
if (err) {
console.log(err);
}
if (--wait === 0 && cb) {
cb();
}
}
};
1
SQLite3.prototype.alterTable = function (model, actualFields, done) {
var self = this;
var m = this._models[model];
var propNames = Object.keys(m.properties);
var sql = [];
// change/add new fields
propNames.forEach(function (propName) {
var found;
actualFields.forEach(function (f) {
if (f.Field === propName) {
found = f;
}
});
if (found) {
actualize(propName, found);
} else {
sql.push('ADD COLUMN `' + propName + '` ' + self.propertySettingsSQL(model, propName));
}
});
// drop columns
actualFields.forEach(function (f) {
var notFound = !~propNames.indexOf(f.Field);
if (f.Field === 'id') return;
if (notFound || !m.properties[f.Field]) {
sql.push('DROP COLUMN `' + f.Field + '`');
}
});
if (sql.length) {
this.command('ALTER TABLE ' + this.tableEscaped(model) + ' ' + sql.join(',\n'), done);
} else {
done();
}
function actualize(propName, oldSettings) {
var newSettings = m.properties[propName];
if (newSettings && changed(newSettings, oldSettings)) {
sql.push('CHANGE COLUMN `' + propName + '` `' + propName + '` ' + self.propertySettingsSQL(model, propName));
}
}
function changed(newSettings, oldSettings) {
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true;
return false;
}
};
1
SQLite3.prototype.propertiesSQL = function (model) {
var self = this;
3 var sql = ['`id` INTEGER PRIMARY KEY'];
3 Object.keys(this._models[model].properties).forEach(function (prop) {
sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
14 });
3 return sql.join(',\n ');
3
};
1
SQLite3.prototype.propertySettingsSQL = function (model, prop) {
var p = this._models[model].properties[prop];
14 return datatype(p) + ' ' +
14 (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
};
1
function datatype(p) {
switch (p.type.name) {
case 'String':
return 'VARCHAR(' + (p.limit || 255) + ')';
5 case 'Text':
return 'TEXT';
2 case 'Number':
return 'INT(11)';
3 case 'Date':
return 'DATETIME';
2 case 'Boolean':
return 'TINYINT(1)';
2 }
}
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var pg = safeRequire('pg');
1var BaseSQL = require('../sql');
1
exports.initialize = function initializeSchema(schema, callback) {
if (!pg) return;
1
var Client = pg.Client;
1 var s = schema.settings;
1 schema.client = new Client(s.url ? s.url : {
host: s.host || 'localhost',
port: s.port || 5432,
user: s.username,
password: s.password,
database: s.database,
debug: s.debug
});
1 schema.adapter = new PG(schema.client);
1
schema.adapter.connect(callback);
1};
1
function PG(client) {
this._models = {};
1 this.client = client;
1}
require('util').inherits(PG, BaseSQL);
1
PG.prototype.connect = function (callback) {
this.client.connect(function (err) {
if (!err){
callback();
1 }else{
console.error(err);
throw err;
}
});
1};
1
PG.prototype.query = function (sql, callback) {
var time = Date.now();
91 var log = this.log;
91 this.client.query(sql, function (err, data) {
if (log) log(sql, time);
91 callback(err, data ? data.rows : null);
91 });
91};
1
/**
* Must invoke callback(err, id)
*/
PG.prototype.create = function (model, data, callback) {
var fields = this.toFields(model, data, true);
38 var sql = 'INSERT INTO ' + this.tableEscaped(model) + '';
38 if (fields) {
sql += ' ' + fields;
38 } else {
sql += ' VALUES ()';
}
sql += ' RETURNING id';
38 this.query(sql, function (err, info) {
if (err) return callback(err);
38 callback(err, info && info[0] && info[0].id);
38 });
38};
1
PG.prototype.updateOrCreate = function (model, data, callback) {
var pg = this;
2 var fieldsNames = [];
2 var fieldValues = [];
2 var combined = [];
2 var props = this._models[model].properties;
2 Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '"' + key + '"';
12 var v;
12 if (key !== 'id') {
v = pg.toDatabase(props[key], data[key]);
10 } else {
v = data[key];
2 }
fieldsNames.push(k);
12 fieldValues.push(v);
12 if (key !== 'id') combined.push(k + ' = ' + v);
12 }
});
2
var sql = 'UPDATE ' + this.tableEscaped(model);
2 sql += ' SET ' + combined + ' WHERE id = ' + data.id + ';';
2 sql += ' INSERT INTO ' + this.tableEscaped(model);
2 sql += ' (' + fieldsNames.join(', ') + ')';
2 sql += ' SELECT ' + fieldValues.join(', ')
sql += ' WHERE NOT EXISTS (SELECT 1 FROM ' + this.tableEscaped(model);
2 sql += ' WHERE id = ' + data.id + ') RETURNING id';
2
this.query(sql, function (err, info) {
if (!err && info && info[0] && info[0].id) {
data.id = info[0].id;
2 }
callback(err, data);
2 });
2};
1
PG.prototype.toFields = function (model, data, forCreate) {
var fields = [];
43 var props = this._models[model].properties;
43
if(forCreate){
var columns = [];
38 Object.keys(data).forEach(function (key) {
if (props[key]) {
columns.push('"' + key + '"');
214 fields.push(this.toDatabase(props[key], data[key]));
214 }
}.bind(this));
38 return '(' + columns.join(',') + ') VALUES ('+fields.join(',')+')';
38 }else{
Object.keys(data).forEach(function (key) {
if (props[key]) {
fields.push('"' + key + '" = ' + this.toDatabase(props[key], data[key]));
18 }
}.bind(this));
5 return fields.join(',');
5 }
};
1
function dateToPostgres(val) {
return [
39 val.getUTCFullYear(),
fz(val.getUTCMonth() + 1),
fz(val.getUTCDate())
].join('-') + ' ' + [
fz(val.getUTCHours()),
fz(val.getUTCMinutes()),
fz(val.getUTCSeconds())
].join(':');
function fz(v) {
return v < 10 ? '0' + v : v;
195 }
}
PG.prototype.toDatabase = function (prop, val) {
if (val === null) {
// Postgres complains with NULLs in not null columns
// If we have an autoincrement value, return DEFAULT instead
if( prop.autoIncrement ) {
return 'DEFAULT';
}
else {
return 'NULL';
141 }
}
if (val.constructor.name === 'Object') {
var operator = Object.keys(val)[0]
val = val[operator];
5 if (operator === 'between') {
return this.toDatabase(prop, val[0]) + ' AND ' + this.toDatabase(prop, val[1]);
1 }
}
if (prop.type.name === 'Number') return val;
117 if (prop.type.name === 'Date') {
if (!val) {
if( prop.autoIncrement ) {
return 'DEFAULT';
}
else {
return 'NULL';
}
}
if (!val.toUTCString) {
val = new Date(val);
14 }
return escape(dateToPostgres(val));
39 }
return escape(val.toString());
78
};
1
PG.prototype.fromDatabase = function (model, data) {
if (!data) return null;
8 var props = this._models[model].properties;
8 Object.keys(data).forEach(function (key) {
var val = data[key];
52 if (props[key]) {
// if (props[key])
}
data[key] = val;
52 });
8 return data;
8};
1
PG.prototype.escapeName = function (name) {
return '"' + name.replace(/\./g, '"."') + '"';
114};
1
PG.prototype.all = function all(model, filter, callback) {
this.query('SELECT * FROM ' + this.tableEscaped(model) + ' ' + this.toFilter(model, filter), function (err, data) {
if (err) {
return callback(err, []);
}
callback(err, data);
21 }.bind(this));
21};
1
PG.prototype.toFilter = function (model, filter) {
if (filter && typeof filter.where === 'function') {
return filter();
}
if (!filter) return '';
19 var props = this._models[model].properties;
19 var out = '';
19 if (filter.where) {
var fields = [];
16 var conds = filter.where;
16 Object.keys(conds).forEach(function (key) {
if (filter.where[key] && filter.where[key].constructor.name === 'RegExp') {
return;
}
if (props[key]) {
var filterValue = this.toDatabase(props[key], filter.where[key]);
16 if (filterValue === 'NULL') {
fields.push('"' + key + '" IS ' + filterValue);
1 } else if (conds[key].constructor.name === 'Object') {
var condType = Object.keys(conds[key])[0];
5 var sqlCond = key;
5 switch (condType) {
case 'gt':
sqlCond += ' > ';
1 break;
1 case 'gte':
sqlCond += ' >= ';
1 break;
1 case 'lt':
sqlCond += ' < ';
1 break;
1 case 'lte':
sqlCond += ' <= ';
1 break;
1 case 'between':
sqlCond += ' BETWEEN ';
1 break;
1 }
sqlCond += filterValue;
5 fields.push(sqlCond);
5 } else {
fields.push('"' + key + '" = ' + filterValue);
10 }
}
}.bind(this));
16 if (fields.length) {
out += ' WHERE ' + fields.join(' AND ');
16 }
}
if (filter.order) {
out += ' ORDER BY ' + filter.order;
6 }
if (filter.limit) {
out += ' LIMIT ' + filter.limit + ' ' + (filter.offset || '');
5 }
return out;
19};
1
PG.prototype.autoupdate = function (cb) {
var self = this;
var wait = 0;
Object.keys(this._models).forEach(function (model) {
wait += 1;
self.query('SELECT column_name as Field, udt_name as Type, is_nullable as Null, column_default as Default FROM information_schema.COLUMNS WHERE table_name = \''+ self.table(model) + '\'', function (err, fields) {
self.alterTable(model, fields, done);
});
});
function done(err) {
if (err) {
console.log(err);
}
if (--wait === 0 && cb) {
cb();
}
}
};
1
PG.prototype.alterTable = function (model, actualFields, done) {
var self = this;
var m = this._models[model];
var propNames = Object.keys(m.properties);
var sql = [];
// change/add new fields
propNames.forEach(function (propName) {
var found;
actualFields.forEach(function (f) {
if (f.Field === propName) {
found = f;
}
});
if (found) {
actualize(propName, found);
} else {
sql.push('ADD COLUMN "' + propName + '" ' + self.propertySettingsSQL(model, propName));
}
});
// drop columns
actualFields.forEach(function (f) {
var notFound = !~propNames.indexOf(f.Field);
if (f.Field === 'id') return;
if (notFound || !m.properties[f.Field]) {
sql.push('DROP COLUMN "' + f.Field + '"');
}
});
if (sql.length) {
this.query('ALTER TABLE ' + this.tableEscaped(model) + ' ' + sql.join(',\n'), done);
} else {
done();
}
function actualize(propName, oldSettings) {
var newSettings = m.properties[propName];
if (newSettings && changed(newSettings, oldSettings)) {
sql.push('CHANGE COLUMN "' + propName + '" "' + propName + '" ' + self.propertySettingsSQL(model, propName));
}
}
function changed(newSettings, oldSettings) {
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true;
if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true;
return false;
}
};
1
PG.prototype.propertiesSQL = function (model) {
var self = this;
3 var sql = ['"id" SERIAL NOT NULL UNIQUE PRIMARY KEY'];
3 Object.keys(this._models[model].properties).forEach(function (prop) {
sql.push('"' + prop + '" ' + self.propertySettingsSQL(model, prop));
14 });
3 return sql.join(',\n ');
3
};
1
PG.prototype.propertySettingsSQL = function (model, prop) {
var p = this._models[model].properties[prop];
14 return datatype(p) + ' ' +
14 (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
};
1
function escape(val) {
if (val === undefined || val === null) {
return 'NULL';
}
switch (typeof val) {
case 'boolean': return (val) ? 'true' : 'false';
case 'number': return val+'';
}
if (typeof val === 'object') {
val = (typeof val.toISOString === 'function')
? val.toISOString()
: val.toString();
}
val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) {
switch(s) {
case "\0": return "\\0";
case "\n": return "\\n";
case "\r": return "\\r";
case "\b": return "\\b";
case "\t": return "\\t";
case "\x1a": return "\\Z";
default: return "\\"+s;
}
});
117 return "'"+val+"'";
117};
1
function datatype(p) {
switch (p.type.name) {
case 'String':
return 'VARCHAR(' + (p.limit || 255) + ')';
5 case 'Text':
return 'TEXT';
2 case 'Number':
return 'INTEGER';
3 case 'Date':
return 'TIMESTAMP';
2 case 'Boolean':
return 'BOOLEAN';
2 }
}
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var neo4j = safeRequire('neo4j');
1
exports.initialize = function initializeSchema(schema, callback) {
schema.client = new neo4j.GraphDatabase(schema.settings.url);
1 schema.adapter = new Neo4j(schema.client);
1 process.nextTick(callback);
1};
1
function Neo4j(client) {
this._models = {};
1 this.client = client;
1 this.cache = {};
1}
Neo4j.prototype.define = function defineModel(descr) {
this.mixClassMethods(descr.model, descr.properties);
3 this.mixInstanceMethods(descr.model.prototype, descr.properties);
3 this._models[descr.model.modelName] = descr;
3};
1
Neo4j.prototype.createIndexHelper = function (cls, indexName) {
var db = this.client;
4 var method = 'findBy' + indexName[0].toUpperCase() + indexName.substr(1);
4 cls[method] = function (value, cb) {
db.getIndexedNode(cls.modelName, indexName, value, function (err, node) {
if (err) return cb(err);
1 if (node) {
node.data.id = node.id;
1 cb(null, new cls(node.data));
1 } else {
cb(null, null);
}
});
1 };
4};
1
Neo4j.prototype.mixClassMethods = function mixClassMethods(cls, properties) {
var neo = this;
3
Object.keys(properties).forEach(function (name) {
if (properties[name].index) {
neo.createIndexHelper(cls, name);
4 }
});
3
cls.setupCypherQuery = function (name, queryStr, rowHandler) {
cls[name] = function cypherQuery(params, cb) {
if (typeof params === 'function') {
cb = params;
params = [];
} else if (params.constructor.name !== 'Array') {
params = [params];
}
var i = 0;
var q = queryStr.replace(/\?/g, function () {
return params[i++];
});
neo.client.query(function (err, result) {
if (err) return cb(err, []);
cb(null, result.map(rowHandler));
}, q);
};
};
3
/**
* @param from - id of object to check relation from
* @param to - id of object to check relation to
* @param type - type of relation
* @param direction - all | incoming | outgoing
* @param cb - callback (err, rel || false)
*/
cls.relationshipExists = function relationshipExists(from, to, type, direction, cb) {
neo.node(from, function (err, node) {
if (err) return cb(err);
node._getRelationships(direction, type, function (err, rels) {
if (err && cb) return cb(err);
if (err && !cb) throw err;
var found = false;
if (rels && rels.forEach) {
rels.forEach(function (r) {
if (r.start.id === from && r.end.id === to) {
found = true;
}
});
}
cb && cb(err, found);
});
});
};
3
cls.createRelationshipTo = function createRelationshipTo(id1, id2, type, data, cb) {
var fromNode, toNode;
neo.node(id1, function (err, node) {
if (err && cb) return cb(err);
if (err && !cb) throw err;
fromNode = node;
ok();
});
neo.node(id2, function (err, node) {
if (err && cb) return cb(err);
if (err && !cb) throw err;
toNode = node;
ok();
});
function ok() {
if (fromNode && toNode) {
fromNode.createRelationshipTo(toNode, type, cleanup(data), cb);
}
}
};
3
cls.createRelationshipFrom = function createRelationshipFrom(id1, id2, type, data, cb) {
cls.createRelationshipTo(id2, id1, type, data, cb);
}
// only create relationship if it is not exists
cls.ensureRelationshipTo = function (id1, id2, type, data, cb) {
cls.relationshipExists(id1, id2, type, 'outgoing', function (err, exists) {
if (err && cb) return cb(err);
if (err && !cb) throw err;
if (exists) return cb && cb(null);
cls.createRelationshipTo(id1, id2, type, data, cb);
});
}
};
1
Neo4j.prototype.mixInstanceMethods = function mixInstanceMethods(proto) {
var neo = this;
3
/**
* @param obj - Object or id of object to check relation with
* @param type - type of relation
* @param cb - callback (err, rel || false)
*/
proto.isInRelationWith = function isInRelationWith(obj, type, direction, cb) {
this.constructor.relationshipExists(this.id, obj.id || obj, type, 'all', cb);
};
3};
1
Neo4j.prototype.node = function find(id, callback) {
if (this.cache[id]) {
callback(null, this.cache[id]);
8 } else {
this.client.getNodeById(id, function (err, node) {
if (node) {
this.cache[id] = node;
8 }
callback(err, node);
10 }.bind(this));
10 }
};
1
Neo4j.prototype.create = function create(model, data, callback) {
data.nodeType = model;
30 var node = this.client.createNode();
30 node.data = cleanup(data);
30 node.data.nodeType = model;
30 node.save(function (err) {
if (err) {
return callback(err);
}
this.cache[node.id] = node;
30 node.index(model, 'id', node.id, function (err) {
if (err) return callback(err);
30 this.updateIndexes(model, node, function (err) {
if (err) return callback(err);
30 callback(null, node.id);
30 });
30 }.bind(this));
30 }.bind(this));
30};
1
Neo4j.prototype.updateIndexes = function updateIndexes(model, node, cb) {
var props = this._models[model].properties;
35 var wait = 1;
35 Object.keys(props).forEach(function (key) {
if (props[key].index && node.data[key]) {
wait += 1;
55 node.index(model, key, node.data[key], done);
55 }
});
35
done();
35
var error = false;
35 function done(err) {
error = error || err;
90 if (--wait === 0) {
cb(error);
35 }
}
};
1
Neo4j.prototype.save = function save(model, data, callback) {
var self = this;
5 this.node(data.id, function (err, node) {
if (err) return callback(err);
5 node.data = cleanup(data);
5 node.save(function (err) {
if (err) return callback(err);
5 self.updateIndexes(model, node, function (err) {
if (err) return console.log(err);
5 callback(null, node.data);
5 });
5 });
5 });
5};
1
Neo4j.prototype.exists = function exists(model, id, callback) {
delete this.cache[id];
3 this.node(id, callback);
3};
1
Neo4j.prototype.find = function find(model, id, callback) {
delete this.cache[id];
7 this.node(id, function (err, node) {
if (node && node.data) {
node.data.id = id;
6 }
callback(err, this.readFromDb(model, node && node.data));
7 }.bind(this));
7};
1
Neo4j.prototype.readFromDb = function readFromDb(model, data) {
if (!data) return data;
166 var res = {};
166 var props = this._models[model].properties;
166 Object.keys(data).forEach(function (key) {
if (props[key] && props[key].type.name === 'Date') {
res[key] = new Date(data[key]);
142 } else {
res[key] = data[key];
642 }
});
166 return res;
166};
1
Neo4j.prototype.destroy = function destroy(model, id, callback) {
var force = true;
1 this.node(id, function (err, node) {
if (err) return callback(err);
1 node.delete(function (err) {
if (err) return callback(err);
1 delete this.cache[id];
}.bind(this), force);
1 });
1};
1
Neo4j.prototype.all = function all(model, filter, callback) {
this.client.queryNodeIndex(model, 'id:*', function (err, nodes) {
if (nodes) {
nodes = nodes.map(function (obj) {
obj.data.id = obj.id;
160 return this.readFromDb(model, obj.data);
160 }.bind(this));
19 }
if (filter) {
nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes;
17 if (filter.order) {
var key = filter.order.split(' ')[0];
6 var dir = filter.order.split(' ')[1];
6 nodes = nodes.sort(function (a, b) {
return a[key] > b[key];
49 });
6 if (dir === 'DESC') nodes = nodes.reverse();
6 }
}
callback(err, nodes);
19 }.bind(this));
19};
1
Neo4j.prototype.allNodes = function all(model, callback) {
this.client.queryNodeIndex(model, 'id:*', function (err, nodes) {
callback(err, nodes);
2 });
2};
1
function applyFilter(filter) {
if (typeof filter.where === 'function') {
return filter.where;
}
var keys = Object.keys(filter.where || {});
17 return function (obj) {
17 var pass = true;
145 keys.forEach(function (key) {
if (!test(filter.where[key], obj[key])) {
pass = false;
90 }
});
145 return pass;
145 }
function test(example, value) {
if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
return value.match(example);
}
if (typeof value === 'object' && value.constructor.name === 'Date' && typeof example === 'object' && example.constructor.name === 'Date') {
return example.toString() === value.toString();
10 }
// not strict equality
return example == value;
104 }
}
Neo4j.prototype.destroyAll = function destroyAll(model, callback) {
var wait, error = null;
2 this.allNodes(model, function (err, collection) {
if (err) return callback(err);
2 wait = collection.length;
2 collection && collection.forEach && collection.forEach(function (node) {
node.delete(done, true);
29 });
2 });
2
function done(err) {
error = error || err;
29 if (--wait === 0) {
callback(error);
2 }
}
};
1
Neo4j.prototype.count = function count(model, callback, conds) {
this.all(model, {where: conds}, function (err, collection) {
callback(err, collection ? collection.length : 0);
3 });
3};
1
Neo4j.prototype.updateAttributes = function updateAttributes(model, id, data, cb) {
data.id = id;
2 this.node(id, function (err, node) {
this.save(model, merge(node.data, data), cb);
2 }.bind(this));
2};
1
function cleanup(data) {
if (!data) return null;
35 var res = {};
35 Object.keys(data).forEach(function (key) {
var v = data[key];
263 if (v === null) {
// skip
// console.log('skip null', key);
} else if (v && v.constructor.name === 'Array' && v.length === 0) {
// skip
// console.log('skip blank array', key);
} else if (typeof v !== 'undefined') {
res[key] = v;
121 }
});
35 return res;
35}
function merge(base, update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
5 });
2 return base;
2}
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();
13 }
}
/**
* Length validator
*/
function validateLength(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
10
var len = this[attr].length;
10 if (conf.min && len < conf.min) {
err('min');
1 }
if (conf.max && len > conf.max) {
err('max');
1 }
if (conf.is && len !== conf.is) {
err('is');
3 }
}
/**
* Numericality validator
*/
function validateNumericality(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
22
if (typeof this[attr] !== 'number') {
return err('number');
1 }
if (conf.int && this[attr] !== Math.round(this[attr])) {
return err('int');
1 }
}
/**
* Inclusion validator
*/
function validateInclusion(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
19
if (!~conf.in.indexOf(this[attr])) {
err()
}
}
/**
* Exclusion validator
*/
function validateExclusion(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
15
if (~conf.in.indexOf(this[attr])) {
err()
}
}
/**
* Format validator
*/
function validateFormat(attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;
13
if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
1 }
} else {
err();
}
}
/**
* Custom validator
*/
function validateCustom(attr, conf, err, done) {
conf.customValidator.call(this, err, done);
242}
/**
* Uniqueness validator
*/
function validateUniqueness(attr, conf, err, done) {
var cond = {where: {}};
6 cond.where[attr] = this[attr];
6 this.constructor.all(cond, function (error, found) {
if (found.length > 1) {
err();
} else if (found.length === 1 && found[0].id != this.id) {
err();
1 }
done();
6 }.bind(this));
6}
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);
21 };
}
/**
* 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;
373
// exit with success when no errors
if (!this.constructor._validations) {
cleanErrors(this);
107 if (callback) {
callback(valid);
107 }
return valid;
107 }
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: new Errors
});
266
this.trigger('validation', function (validationsDone) {
var inst = this;
266 this.constructor._validations.forEach(function (v) {
if (v[2] && v[2].async) {
async = true;
237 wait += 1;
237 validationFailed(inst, v, done);
237 } else {
if (validationFailed(inst, v)) {
valid = false;
26 }
}
});
266
if (!async) {
validationsDone();
35 }
var asyncFail = false;
266 function done(fail) {
asyncFail = asyncFail || fail;
237 if (--wait === 0 && callback) {
validationsDone.call(inst, function () {
if( valid && !asyncFail ) cleanErrors(inst);
230 callback(valid && !asyncFail);
230 });
230 }
}
});
266
if (!async) {
if (valid) cleanErrors(this);
35 if (callback) callback(valid);
34 return valid;
34 } else {
// in case of async validation we should return undefined here,
// because not all validations are finished yet
return;
231 }
};
1
function cleanErrors(inst) {
Object.defineProperty(inst, 'errors', {
enumerable: false,
configurable: true,
value: false
});
350}
function validationFailed(inst, v, cb) {
var attr = v[0];
579 var conf = v[1];
579 var opts = v[2] || {};
579
if (typeof attr !== 'string') return false;
579
// here we should check skip validation conditions (if, unless)
// that can be specified in conf
if (skipValidation(inst, conf, 'if')) return false;
544 if (skipValidation(inst, conf, 'unless')) return false;
472
var fail = false;
472 var validator = validators[conf.validation];
472 var validatorArguments = [];
472 validatorArguments.push(attr);
472 validatorArguments.push(conf);
472 validatorArguments.push(function onerror(kind) {
var message;
28 if (conf.message) {
message = conf.message;
3 }
if (!message && defaultMessages[conf.validation]) {
message = defaultMessages[conf.validation];
24 }
if (!message) {
message = 'is invalid';
1 }
if (kind) {
if (message[kind]) {
// get deeper
message = message[kind];
10 } else if (defaultMessages.common[kind]) {
message = defaultMessages.common[kind];
}
}
inst.errors.add(attr, message);
28 fail = true;
28 });
472 if (cb) {
validatorArguments.push(function () {
cb(fail);
237 });
237 }
validator.apply(inst, validatorArguments);
472 return fail;
472}
function skipValidation(inst, conf, kind) {
var doValidate = true;
1123 if (typeof conf[kind] === 'function') {
doValidate = conf[kind].call(inst);
36 if (kind === 'unless') doValidate = !doValidate;
36 } else if (typeof conf[kind] === 'string') {
if (inst.hasOwnProperty(conf[kind])) {
doValidate = inst[conf[kind]];
45 if (kind === 'unless') doValidate = !doValidate;
45 } else if (typeof inst[conf[kind]] === 'function') {
doValidate = inst[conf[kind]].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else {
doValidate = kind === 'if';
37 }
}
return !doValidate;
1123}
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);
130 if (isNull) {
if (!conf.allowNull) {
err('null');
}
return true;
25 } else {
if (blank(this[attr])) {
if (!conf.allowBlank) {
err('blank');
}
return true;
26 }
}
return false;
79}
/**
* 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;
199 if (v instanceof Array && v.length === 0) return true;
199 if (v === null) return true;
186 if (typeof v == 'string' && v === '') return true;
160 return false;
160}
function configure(cls, validation, args, opts) {
if (!cls._validations) {
Object.defineProperty(cls, '_validations', {
writable: true,
configurable: true,
enumerable: false,
value: []
});
9 }
args = [].slice.call(args);
21 var conf;
21 if (typeof args[args.length - 1] === 'object') {
conf = args.pop();
11 } else {
conf = {};
10 }
if (validation === 'custom' && typeof args[args.length - 1] === 'function') {
conf.customValidator = args.pop();
10 }
conf.validation = validation;
21 args.forEach(function (attr) {
cls._validations.push([attr, conf, opts]);
22 });
21}
function Errors() {
}
Errors.prototype.add = function (field, message) {
if (!this[field]) {
this[field] = [message];
27 } else {
this[field].push(message);
1 }
};
1
var safeRequire = require('../utils').safeRequire;
/**
* Module dependencies
*/
var mongodb = safeRequire('mongodb');
1var ObjectID = mongodb.ObjectID;
1
exports.initialize = function initializeSchema(schema, callback) {
if (!mongodb) return;
1
var s = schema.settings;
1
if (schema.settings.url) {
var url = require('url').parse(schema.settings.url);
1 s.host = url.hostname;
1 s.port = url.port;
1 s.database = url.pathname.replace(/^\//, '');
1 s.username = url.auth && url.auth.split(':')[0];
1 s.password = url.auth && url.auth.split(':')[1];
1 }
s.host = s.host || 'localhost';
1 s.port = parseInt(s.port || '27017', 10);
1 s.database = s.database || 'test';
1
schema.adapter = new MongoDB(s, schema, callback);
1};
1
function MongoDB(s, schema, callback) {
this._models = {};
1 this.collections = {};
1
var server = new mongodb.Server(s.host, s.port, {});
1 new mongodb.Db(s.database, server, {}).open(function (err, client) {
if (err) throw err;
1 this.client = client;
1 schema.client = client;
1 callback();
1 }.bind(this));
1}
MongoDB.prototype.define = function (descr) {
if (!descr.settings) descr.settings = {};
3 this._models[descr.model.modelName] = descr;
3};
1
MongoDB.prototype.defineProperty = function (model, prop, params) {
this._models[model].properties[prop] = params;
};
1
MongoDB.prototype.collection = function (name) {
if (!this.collections[name]) {
this.collections[name] = new mongodb.Collection(this.client, name);
2 }
return this.collections[name];
87};
1
MongoDB.prototype.create = function (model, data, callback) {
this.collection(model).insert(data, {}, function (err, m) {
callback(err, err ? null : m[0]._id.toString());
40 });
40};
1
MongoDB.prototype.save = function (model, data, callback) {
this.collection(model).save({_id: new ObjectID(data.id)}, data, function (err) {
callback(err);
3 });
3};
1
MongoDB.prototype.exists = function (model, id, callback) {
this.collection(model).findOne({_id: new ObjectID(id)}, function (err, data) {
callback(err, !err && data);
3 });
3};
1
MongoDB.prototype.find = function find(model, id, callback) {
this.collection(model).findOne({_id: new ObjectID(id)}, function (err, data) {
if (data) data.id = id;
11 callback(err, data);
11 });
11};
1
MongoDB.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
var adapter = this;
2 if (!data.id) return this.create(data, callback);
2 this.find(model, data.id, function (err, inst) {
if (err) return callback(err);
2 if (inst) {
adapter.updateAttributes(model, data.id, data, callback);
} else {
delete data.id;
2 adapter.create(model, data, function (err, id) {
if (err) return callback(err);
2 if (id) {
data.id = id;
2 delete data._id;
2 callback(null, data);
2 } else{
callback(null, null); // wtf?
}
});
2 }
});
2};
1
MongoDB.prototype.destroy = function destroy(model, id, callback) {
this.collection(model).remove({_id: new ObjectID(id)}, callback);
1};
1
MongoDB.prototype.all = function all(model, filter, callback) {
if (!filter) {
filter = {};
2 }
var query = {};
21 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[k] = { $gte: cond[0], $lte: cond[1]};
1 } else {
query[k] = {};
4 query[k]['