Removed nosql adapters (moved to own repos)
This commit is contained in:
parent
963d393089
commit
a62aab180d
2
index.js
2
index.js
|
@ -22,3 +22,5 @@ try {
|
|||
}
|
||||
} catch (e) {}
|
||||
|
||||
exports.test = require('./test/common_test');
|
||||
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
|
||||
var safeRequire = require('../utils').safeRequire;
|
||||
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
var mongodb = safeRequire('mongodb');
|
||||
var ObjectID = mongodb.ObjectID;
|
||||
|
||||
exports.initialize = function initializeSchema(schema, callback) {
|
||||
if (!mongodb) return;
|
||||
|
||||
var s = schema.settings;
|
||||
|
||||
if (schema.settings.rs) {
|
||||
|
||||
s.rs = schema.settings.rs;
|
||||
if (schema.settings.url) {
|
||||
var uris = schema.settings.url.split(',');
|
||||
s.hosts = []
|
||||
s.ports = []
|
||||
uris.forEach(function(uri) {
|
||||
var url = require('url').parse(uri);
|
||||
|
||||
s.hosts.push(url.hostname || 'localhost');
|
||||
s.ports.push(parseInt(url.port || '27017', 10));
|
||||
|
||||
if (!s.database) s.database = url.pathname.replace(/^\//, '');
|
||||
if (!s.username) s.username = url.auth && url.auth.split(':')[0];
|
||||
if (!s.password) s.password = url.auth && url.auth.split(':')[1];
|
||||
});
|
||||
}
|
||||
|
||||
s.database = s.database || 'test';
|
||||
|
||||
} else {
|
||||
|
||||
if (schema.settings.url) {
|
||||
var url = require('url').parse(schema.settings.url);
|
||||
s.host = url.hostname;
|
||||
s.port = url.port;
|
||||
s.database = url.pathname.replace(/^\//, '');
|
||||
s.username = url.auth && url.auth.split(':')[0];
|
||||
s.password = url.auth && url.auth.split(':')[1];
|
||||
}
|
||||
|
||||
s.host = s.host || 'localhost';
|
||||
s.port = parseInt(s.port || '27017', 10);
|
||||
s.database = s.database || 'test';
|
||||
|
||||
}
|
||||
|
||||
s.safe = s.safe || false;
|
||||
|
||||
schema.adapter = new MongoDB(s, schema, callback);
|
||||
};
|
||||
|
||||
function MongoDB(s, schema, callback) {
|
||||
var i, n;
|
||||
this._models = {};
|
||||
this.collections = {};
|
||||
|
||||
var server;
|
||||
if (s.rs) {
|
||||
set = [];
|
||||
for (i = 0, n = s.hosts.length; i < n; i++) {
|
||||
set.push(new mongodb.Server(s.hosts[i], s.ports[i], {auto_reconnect: true}));
|
||||
}
|
||||
server = new mongodb.ReplSetServers(set, {rs_name: s.rs});
|
||||
|
||||
} else {
|
||||
server = new mongodb.Server(s.host, s.port, {});
|
||||
}
|
||||
|
||||
new mongodb.Db(s.database, server, { safe: s.safe }).open(function (err, client) {
|
||||
if (err) throw err;
|
||||
if (s.username && s.password) {
|
||||
t = this;
|
||||
client.authenticate(s.username, s.password, function (err, result) {
|
||||
t.client = client;
|
||||
schema.client = client;
|
||||
callback();
|
||||
});
|
||||
|
||||
} else {
|
||||
this.client = client;
|
||||
schema.client = client;
|
||||
callback();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
MongoDB.prototype.define = function (descr) {
|
||||
if (!descr.settings) descr.settings = {};
|
||||
this._models[descr.model.modelName] = descr;
|
||||
};
|
||||
|
||||
MongoDB.prototype.defineProperty = function (model, prop, params) {
|
||||
this._models[model].properties[prop] = params;
|
||||
};
|
||||
|
||||
MongoDB.prototype.collection = function (name) {
|
||||
if (!this.collections[name]) {
|
||||
this.collections[name] = new mongodb.Collection(this.client, name);
|
||||
}
|
||||
return this.collections[name];
|
||||
};
|
||||
|
||||
MongoDB.prototype.create = function (model, data, callback) {
|
||||
if (data.id === null) {
|
||||
delete data.id;
|
||||
}
|
||||
this.collection(model).insert(data, {}, function (err, m) {
|
||||
callback(err, err ? null : m[0]._id.toString());
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.save = function (model, data, callback) {
|
||||
this.collection(model).update({_id: new ObjectID(data.id)}, data, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.exists = function (model, id, callback) {
|
||||
this.collection(model).findOne({_id: new ObjectID(id)}, function (err, data) {
|
||||
callback(err, !err && data);
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.find = function find(model, id, callback) {
|
||||
this.collection(model).findOne({_id: new ObjectID(id)}, function (err, data) {
|
||||
if (data) data.id = id;
|
||||
callback(err, data);
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
|
||||
var adapter = this;
|
||||
if (!data.id) return this.create(data, callback);
|
||||
this.find(model, data.id, function (err, inst) {
|
||||
if (err) return callback(err);
|
||||
if (inst) {
|
||||
adapter.updateAttributes(model, data.id, data, callback);
|
||||
} else {
|
||||
delete data.id;
|
||||
adapter.create(model, data, function (err, id) {
|
||||
if (err) return callback(err);
|
||||
if (id) {
|
||||
data.id = id;
|
||||
delete data._id;
|
||||
callback(null, data);
|
||||
} else{
|
||||
callback(null, null); // wtf?
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.destroy = function destroy(model, id, callback) {
|
||||
this.collection(model).remove({_id: new ObjectID(id)}, callback);
|
||||
};
|
||||
|
||||
MongoDB.prototype.all = function all(model, filter, callback) {
|
||||
if (!filter) {
|
||||
filter = {};
|
||||
}
|
||||
var query = {};
|
||||
if (filter.where) {
|
||||
Object.keys(filter.where).forEach(function (k) {
|
||||
var cond = filter.where[k];
|
||||
var spec = false;
|
||||
if (cond && cond.constructor.name === 'Object') {
|
||||
spec = Object.keys(cond)[0];
|
||||
cond = cond[spec];
|
||||
}
|
||||
if (spec) {
|
||||
if (spec === 'between') {
|
||||
query[k] = { $gte: cond[0], $lte: cond[1]};
|
||||
} else {
|
||||
query[k] = {};
|
||||
query[k]['$' + spec] = cond;
|
||||
}
|
||||
} else {
|
||||
if (cond === null) {
|
||||
query[k] = {$type: 10};
|
||||
} else {
|
||||
query[k] = cond;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
var cursor = this.collection(model).find(query);
|
||||
|
||||
if (filter.order) {
|
||||
var m = filter.order.match(/\s+(A|DE)SC$/);
|
||||
var key = filter.order;
|
||||
var reverse = false;
|
||||
if (m) {
|
||||
key = key.replace(/\s+(A|DE)SC$/, '');
|
||||
if (m[1] === 'DE') reverse = true;
|
||||
}
|
||||
if (reverse) {
|
||||
cursor.sort([[key, 'desc']]);
|
||||
} else {
|
||||
cursor.sort(key);
|
||||
}
|
||||
}
|
||||
if (filter.limit) {
|
||||
cursor.limit(filter.limit);
|
||||
}
|
||||
if (filter.skip) {
|
||||
cursor.skip(filter.skip);
|
||||
} else if (filter.offset) {
|
||||
cursor.skip(filter.offset);
|
||||
}
|
||||
cursor.toArray(function (err, data) {
|
||||
if (err) return callback(err);
|
||||
callback(null, data.map(function (o) { o.id = o._id.toString(); delete o._id; return o; }));
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.destroyAll = function destroyAll(model, callback) {
|
||||
this.collection(model).remove({}, callback);
|
||||
};
|
||||
|
||||
MongoDB.prototype.count = function count(model, callback, where) {
|
||||
this.collection(model).count(where, function (err, count) {
|
||||
callback(err, count);
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
|
||||
this.collection(model).findAndModify({_id: new ObjectID(id)}, [['_id','asc']], {$set: data}, {}, function(err, object) {
|
||||
cb(err, object);
|
||||
});
|
||||
};
|
||||
|
||||
MongoDB.prototype.disconnect = function () {
|
||||
this.client.close();
|
||||
};
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
var safeRequire = require('../utils').safeRequire;
|
||||
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
var mongoose = safeRequire('mongoose');
|
||||
|
||||
exports.initialize = function initializeSchema(schema, callback) {
|
||||
console.error('WARN: mongoose adapter is not supported, please use "mongodb" adapter instead');
|
||||
if (!mongoose) return;
|
||||
|
||||
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;
|
||||
}
|
||||
if (!schema.settings.rs) {
|
||||
schema.client = mongoose.connect(schema.settings.url);
|
||||
} else {
|
||||
schema.client = mongoose.connectSet(schema.settings.url, {rs_name: schema.settings.rs});
|
||||
}
|
||||
|
||||
schema.adapter = new MongooseAdapter(schema.client);
|
||||
process.nextTick(callback);
|
||||
};
|
||||
|
||||
function MongooseAdapter(client) {
|
||||
this._models = {};
|
||||
this.client = client;
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
MongooseAdapter.prototype.define = function (descr) {
|
||||
var props = {};
|
||||
Object.keys(descr.properties).forEach(function (key) {
|
||||
props[key] = {};
|
||||
props[key].type = descr.properties[key].type;
|
||||
if (props[key].type.name === 'Text' || props[key].type.name === 'JSON') {
|
||||
props[key].type = String;
|
||||
}
|
||||
if (props[key].type.name === 'Object') {
|
||||
props[key].type = mongoose.Schema.Types.Mixed;
|
||||
}
|
||||
if (descr.properties[key].index) {
|
||||
props[key].index = descr.properties[key].index;
|
||||
}
|
||||
});
|
||||
var schema = new mongoose.Schema(props);
|
||||
this._models[descr.model.modelName] = mongoose.model(descr.model.modelName, schema, descr.settings.table || null);
|
||||
this.cache[descr.model.modelName] = {};
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.defineForeignKey = function (model, key, cb) {
|
||||
var piece = {};
|
||||
piece[key] = {type: mongoose.Schema.ObjectId, index: true};
|
||||
this._models[model].schema.add(piece);
|
||||
cb(null, String);
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.setCache = function (model, instance) {
|
||||
this.cache[model][instance.id] = instance;
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.getCached = function (model, id, cb) {
|
||||
if (this.cache[model][id]) {
|
||||
cb(null, this.cache[model][id]);
|
||||
} else {
|
||||
this._models[model].findById(id, function (err, instance) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
this.cache[model][id] = instance;
|
||||
cb(null, instance);
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.create = function (model, data, callback) {
|
||||
var m = new this._models[model](data);
|
||||
m.save(function (err) {
|
||||
callback(err, err ? null : m.id);
|
||||
});
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.save = function (model, data, callback) {
|
||||
this.getCached(model, data.id, function (err, inst) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
merge(inst, data);
|
||||
inst.save(callback);
|
||||
});
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.exists = function (model, id, callback) {
|
||||
delete this.cache[model][id];
|
||||
this.getCached(model, id, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(err, !!data);
|
||||
});
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.find = function find(model, id, callback) {
|
||||
delete this.cache[model][id];
|
||||
this.getCached(model, id, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(err, data ? data.toObject() : null);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.all = function all(model, filter, callback) {
|
||||
if (!filter) {
|
||||
filter = {};
|
||||
}
|
||||
var query = this._models[model].find({});
|
||||
if (filter.where) {
|
||||
Object.keys(filter.where).forEach(function (k) {
|
||||
var cond = filter.where[k];
|
||||
var spec = false;
|
||||
if (cond && cond.constructor.name === 'Object') {
|
||||
spec = Object.keys(cond)[0];
|
||||
cond = cond[spec];
|
||||
}
|
||||
if (spec) {
|
||||
if (spec === 'between') {
|
||||
query.where(k).gte(cond[0]).lte(cond[1]);
|
||||
} else {
|
||||
query.where(k)[spec](cond);
|
||||
}
|
||||
} else {
|
||||
query.where(k, cond);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (filter.order) {
|
||||
var keys = filter.order; // can be Array or String
|
||||
if (typeof(keys) == "string") {
|
||||
keys = keys.split(',');
|
||||
}
|
||||
var args = [];
|
||||
|
||||
for(index in keys) {
|
||||
var m = keys[index].match(/\s+(A|DE)SC$/);
|
||||
|
||||
keys[index] = keys[index].replace(/\s+(A|DE)SC$/, '');
|
||||
if (m && m[1] === 'DE') {
|
||||
query.sort(keys[index].trim(), -1);
|
||||
} else {
|
||||
query.sort(keys[index].trim(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filter.limit) {
|
||||
query.limit(filter.limit);
|
||||
}
|
||||
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);
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.destroyAll = function destroyAll(model, callback) {
|
||||
var wait = 0;
|
||||
this._models[model].find(function (err, data) {
|
||||
if (err) return callback(err);
|
||||
wait = data.length;
|
||||
if (!data.length) return callback(null);
|
||||
data.forEach(function (obj) {
|
||||
obj.remove(done)
|
||||
});
|
||||
});
|
||||
|
||||
var error = null;
|
||||
function done(err) {
|
||||
error = error || err;
|
||||
if (--wait === 0) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.count = function count(model, callback, where) {
|
||||
this._models[model].count(where || {}, callback);
|
||||
};
|
||||
|
||||
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);
|
||||
inst.save(cb);
|
||||
} else cb();
|
||||
});
|
||||
};
|
||||
|
||||
MongooseAdapter.prototype.disconnect = function () {
|
||||
this.client.connection.close();
|
||||
};
|
||||
|
||||
function merge(base, update) {
|
||||
Object.keys(update).forEach(function (key) {
|
||||
base[key] = update[key];
|
||||
});
|
||||
return base;
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
// Generated by CoffeeScript 1.4.0
|
||||
(function() {
|
||||
var NanoAdapter, helpers, _,
|
||||
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
||||
__slice = [].slice;
|
||||
|
||||
_ = require('lodash')._;
|
||||
|
||||
exports.initialize = function(schema, callback) {
|
||||
var db, design, opts;
|
||||
if (!(opts = schema.settings)) {
|
||||
throw new Error('url is missing');
|
||||
}
|
||||
db = require('nano')(opts);
|
||||
schema.adapter = new NanoAdapter(db);
|
||||
design = {
|
||||
views: {
|
||||
by_model: {
|
||||
map: 'function (doc) { if (doc.model) return emit(doc.model, null); }'
|
||||
}
|
||||
}
|
||||
};
|
||||
return db.insert(design, '_design/nano', function(err, doc) {
|
||||
return callback();
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter = (function() {
|
||||
|
||||
function NanoAdapter(db) {
|
||||
this.db = db;
|
||||
this.all = __bind(this.all, this);
|
||||
|
||||
this.fromDB = __bind(this.fromDB, this);
|
||||
|
||||
this.forDB = __bind(this.forDB, this);
|
||||
|
||||
this.destroyAll = __bind(this.destroyAll, this);
|
||||
|
||||
this.count = __bind(this.count, this);
|
||||
|
||||
this.updateAttributes = __bind(this.updateAttributes, this);
|
||||
|
||||
this.destroy = __bind(this.destroy, this);
|
||||
|
||||
this.find = __bind(this.find, this);
|
||||
|
||||
this.exists = __bind(this.exists, this);
|
||||
|
||||
this.updateOrCreate = __bind(this.updateOrCreate, this);
|
||||
|
||||
this.save = __bind(this.save, this);
|
||||
|
||||
this.create = __bind(this.create, this);
|
||||
|
||||
this.define = __bind(this.define, this);
|
||||
|
||||
this._models = {};
|
||||
}
|
||||
|
||||
NanoAdapter.prototype.define = function(descr) {
|
||||
var m;
|
||||
m = descr.model.modelName;
|
||||
descr.properties._rev = {
|
||||
type: String
|
||||
};
|
||||
return this._models[m] = descr;
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.create = function() {
|
||||
var args;
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
return this.save.apply(this, args);
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.save = function(model, data, callback) {
|
||||
var _this = this;
|
||||
data.model = model;
|
||||
helpers.savePrep(data);
|
||||
return this.db.insert(this.forDB(model, data), function(err, doc) {
|
||||
return callback(err, doc.id, doc.rev);
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.updateOrCreate = function(model, data, callback) {
|
||||
var _this = this;
|
||||
return this.exists(model, data.id, function(err, exists) {
|
||||
if (exists) {
|
||||
return _this.save(model, data, callback);
|
||||
} else {
|
||||
return _this.create(model, data, function(err, id) {
|
||||
data.id = id;
|
||||
return callback(err, data);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.exists = function(model, id, callback) {
|
||||
return this.db.head(id, function(err, _, headers) {
|
||||
if (err) {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(null, headers != null);
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.find = function(model, id, callback) {
|
||||
var _this = this;
|
||||
return this.db.get(id, function(err, doc) {
|
||||
return callback(err, _this.fromDB(model, doc));
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.destroy = function(model, id, callback) {
|
||||
var _this = this;
|
||||
return this.db.get(id, function(err, doc) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return _this.db.destroy(id, doc._rev, function(err, doc) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback.removed = true;
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.updateAttributes = function(model, id, data, callback) {
|
||||
var _this = this;
|
||||
return this.db.get(id, function(err, base) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return _this.save(model, helpers.merge(base, data), callback);
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.count = function(model, callback, where) {
|
||||
var _this = this;
|
||||
return this.all(model, {
|
||||
where: where
|
||||
}, function(err, docs) {
|
||||
return callback(err, docs.length);
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.destroyAll = function(model, callback) {
|
||||
var _this = this;
|
||||
return this.all(model, {}, function(err, docs) {
|
||||
var doc;
|
||||
docs = (function() {
|
||||
var _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = docs.length; _i < _len; _i++) {
|
||||
doc = docs[_i];
|
||||
_results.push({
|
||||
_id: doc.id,
|
||||
_rev: doc._rev,
|
||||
_deleted: true
|
||||
});
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
return _this.db.bulk({
|
||||
docs: docs
|
||||
}, function(err, body) {
|
||||
return callback(err, body);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.forDB = function(model, data) {
|
||||
var k, props, v;
|
||||
if (data == null) {
|
||||
data = {};
|
||||
}
|
||||
props = this._models[model].properties;
|
||||
for (k in props) {
|
||||
v = props[k];
|
||||
if (data[k] && props[k].type.name === 'Date' && (data[k].getTime != null)) {
|
||||
data[k] = data[k].getTime();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.fromDB = function(model, data) {
|
||||
var date, k, props, v;
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
props = this._models[model].properties;
|
||||
for (k in props) {
|
||||
v = props[k];
|
||||
if ((data[k] != null) && props[k].type.name === 'Date') {
|
||||
date = new Date(data[k]);
|
||||
date.setTime(data[k]);
|
||||
data[k] = date;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
NanoAdapter.prototype.all = function(model, filter, callback) {
|
||||
var params,
|
||||
_this = this;
|
||||
params = {
|
||||
keys: [model],
|
||||
include_docs: true
|
||||
};
|
||||
return this.db.view('nano', 'by_model', params, function(err, body) {
|
||||
var doc, docs, i, k, key, orders, row, sorting, v, where, _i, _len;
|
||||
docs = (function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = body.rows;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
row = _ref[_i];
|
||||
row.doc.id = row.doc._id;
|
||||
delete row.doc._id;
|
||||
_results.push(row.doc);
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
if (where = filter != null ? filter.where : void 0) {
|
||||
for (k in where) {
|
||||
v = where[k];
|
||||
if (_.isDate(v)) {
|
||||
where[k] = v.getTime();
|
||||
}
|
||||
}
|
||||
docs = _.where(docs, where);
|
||||
}
|
||||
if (orders = filter != null ? filter.order : void 0) {
|
||||
if (_.isString(orders)) {
|
||||
orders = [orders];
|
||||
}
|
||||
sorting = function(a, b) {
|
||||
var ak, bk, i, item, rev, _i, _len;
|
||||
for (i = _i = 0, _len = this.length; _i < _len; i = ++_i) {
|
||||
item = this[i];
|
||||
ak = a[this[i].key];
|
||||
bk = b[this[i].key];
|
||||
rev = this[i].reverse;
|
||||
if (ak > bk) {
|
||||
return 1 * rev;
|
||||
}
|
||||
if (ak < bk) {
|
||||
return -1 * rev;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
for (i = _i = 0, _len = orders.length; _i < _len; i = ++_i) {
|
||||
key = orders[i];
|
||||
orders[i] = {
|
||||
reverse: helpers.reverse(key),
|
||||
key: helpers.stripOrder(key)
|
||||
};
|
||||
}
|
||||
docs.sort(sorting.bind(orders));
|
||||
}
|
||||
return callback(err, (function() {
|
||||
var _j, _len1, _results;
|
||||
_results = [];
|
||||
for (_j = 0, _len1 = docs.length; _j < _len1; _j++) {
|
||||
doc = docs[_j];
|
||||
_results.push(this.fromDB(model, doc));
|
||||
}
|
||||
return _results;
|
||||
}).call(_this));
|
||||
});
|
||||
};
|
||||
|
||||
return NanoAdapter;
|
||||
|
||||
})();
|
||||
|
||||
helpers = {
|
||||
merge: function(base, update) {
|
||||
var k, v;
|
||||
if (!base) {
|
||||
return update;
|
||||
}
|
||||
for (k in update) {
|
||||
v = update[k];
|
||||
base[k] = update[k];
|
||||
}
|
||||
return base;
|
||||
},
|
||||
reverse: function(key) {
|
||||
var hasOrder;
|
||||
if (hasOrder = key.match(/\s+(A|DE)SC$/i)) {
|
||||
if (hasOrder[1] === "DE") {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
stripOrder: function(key) {
|
||||
return key.replace(/\s+(A|DE)SC/i, "");
|
||||
},
|
||||
savePrep: function(data) {
|
||||
var id;
|
||||
if (id = data.id) {
|
||||
delete data.id;
|
||||
data._id = id.toString();
|
||||
}
|
||||
if (data._rev === null) {
|
||||
return delete data._rev;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}).call(this);
|
|
@ -1,458 +0,0 @@
|
|||
var safeRequire = require('../utils').safeRequire;
|
||||
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
var redis = safeRequire('redis');
|
||||
|
||||
exports.initialize = function initializeSchema(schema, callback) {
|
||||
console.log('GOOD NEWS! This redis adapter version is deprecated, use redis2 instead. A lot of improvements, and new indexes incompatible with old (sorry about that): now we only store id and not ModelName:id in indexes. Also dates format in indexes changed to unix timestamp for better sorting and filtering performance');
|
||||
if (!redis) return;
|
||||
|
||||
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
|
||||
);
|
||||
schema.client.auth(schema.settings.password);
|
||||
var callbackCalled = false;
|
||||
var database = schema.settings.hasOwnProperty('database') && schema.settings.database;
|
||||
schema.client.on('connect', function () {
|
||||
if (!callbackCalled && database === false) {
|
||||
callbackCalled = true;
|
||||
callback();
|
||||
} else if (database !== false) {
|
||||
if (callbackCalled) {
|
||||
return schema.client.select(schema.settings.database);
|
||||
} else {
|
||||
callbackCalled = true;
|
||||
return schema.client.select(schema.settings.database, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
schema.adapter = new BridgeToRedis(schema.client);
|
||||
};
|
||||
|
||||
function BridgeToRedis(client) {
|
||||
this._models = {};
|
||||
this.client = client;
|
||||
this.indexes = {};
|
||||
}
|
||||
|
||||
BridgeToRedis.prototype.define = function (descr) {
|
||||
var m = descr.model.modelName;
|
||||
this._models[m] = descr;
|
||||
this.indexes[m] = {};
|
||||
Object.keys(descr.properties).forEach(function (prop) {
|
||||
if (descr.properties[prop].index) {
|
||||
this.indexes[m][prop] = descr.properties[prop].type;
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.defineForeignKey = function (model, key, cb) {
|
||||
this.indexes[model][key] = Number;
|
||||
cb(null, Number);
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.save = function (model, data, callback) {
|
||||
deleteNulls(data);
|
||||
var log = this.logger('HMSET ' + model + ':' + data.id + ' ...');
|
||||
this.client.hmset(model + ':' + data.id, data, function (err) {
|
||||
log();
|
||||
if (err) return callback(err);
|
||||
this.updateIndexes(model, data.id, data, callback);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback) {
|
||||
var i = this.indexes[model];
|
||||
var schedule = [['sadd', 's:' + model, id]];
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (i[key]) {
|
||||
schedule.push([
|
||||
'sadd',
|
||||
'i:' + model + ':' + key + ':' + data[key],
|
||||
model + ':' + id
|
||||
]);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
if (schedule.length) {
|
||||
this.client.multi(schedule).exec(function (err) {
|
||||
callback(err, data);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.create = function (model, data, callback) {
|
||||
if (data.id) return create.call(this, data.id, true);
|
||||
|
||||
var log = this.logger('INCR id:' + model);
|
||||
this.client.incr('id:' + model, function (err, id) {
|
||||
log();
|
||||
create.call(this, id);
|
||||
}.bind(this));
|
||||
|
||||
function create(id, upsert) {
|
||||
data.id = id;
|
||||
this.save(model, data, function (err) {
|
||||
if (callback) {
|
||||
callback(err, id);
|
||||
}
|
||||
});
|
||||
|
||||
// push the id to the list of user ids for sorting
|
||||
log('SADD s:' + model + ' ' + data.id);
|
||||
this.client.sadd("s:" + model, upsert ? data : data.id);
|
||||
}
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.updateOrCreate = function (model, data, callback) {
|
||||
if (!data.id) return this.create(model, data, callback);
|
||||
this.save(model, data, callback);
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.exists = function (model, id, callback) {
|
||||
var log = this.logger('EXISTS ' + model + ':' + id);
|
||||
this.client.exists(model + ':' + id, function (err, exists) {
|
||||
log();
|
||||
if (callback) {
|
||||
callback(err, exists);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.find = function find(model, id, callback) {
|
||||
var t1 = Date.now();
|
||||
this.client.hgetall(model + ':' + id, function (err, data) {
|
||||
this.log('HGETALL ' + model + ':' + id, t1);
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
data.id = id;
|
||||
} else {
|
||||
data = null;
|
||||
}
|
||||
callback(err, data);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.destroy = function destroy(model, id, callback) {
|
||||
var t1 = Date.now();
|
||||
this.client.del(model + ':' + id, function (err) {
|
||||
this.log('DEL ' + model + ':' + id, t1);
|
||||
callback(err);
|
||||
}.bind(this));
|
||||
this.log('SREM s:' + model, t1);
|
||||
this.client.srem("s:" + model, id);
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.possibleIndexes = function (model, filter) {
|
||||
if (!filter || Object.keys(filter.where || {}).length === 0) return false;
|
||||
|
||||
var foundIndex = [];
|
||||
var noIndex = [];
|
||||
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]);
|
||||
} else {
|
||||
noIndex.push(key);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
return [foundIndex, noIndex];
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.all = function all(model, filter, callback) {
|
||||
var ts = Date.now();
|
||||
var client = this.client;
|
||||
var log = this.log;
|
||||
var t1 = Date.now();
|
||||
var cmd;
|
||||
var that = this;
|
||||
var sortCmd = [];
|
||||
var props = this._models[model].properties;
|
||||
var allNumeric = true;
|
||||
|
||||
// TODO: we need strict mode when filtration only possible when we have indexes
|
||||
// WHERE
|
||||
if (filter && filter.where) {
|
||||
var pi = this.possibleIndexes(model, filter);
|
||||
var indexes = pi[0];
|
||||
var noIndexes = pi[1];
|
||||
|
||||
if (indexes && indexes.length) {
|
||||
cmd = 'SINTER "' + indexes.join('" "') + '"';
|
||||
if (noIndexes.length) {
|
||||
log(model + ': no indexes found for ', noIndexes.join(', '),
|
||||
'slow sorting and filtering');
|
||||
}
|
||||
indexes.push(noIndexes.length ? orderLimitStageBad : orderLimitStage);
|
||||
client.sinter.apply(client, indexes);
|
||||
} else {
|
||||
// filter manually
|
||||
cmd = 'KEYS ' + model + ':*';
|
||||
client.keys(model + ':*', orderLimitStageBad);
|
||||
}
|
||||
} else {
|
||||
// no filtering, just sort/limit (if any)
|
||||
gotKeys('*');
|
||||
}
|
||||
|
||||
// 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);
|
||||
var t2 = Date.now();
|
||||
if (err) {
|
||||
return callback(err, []);
|
||||
}
|
||||
var query = keys.map(function (key) {
|
||||
return ['hgetall', key];
|
||||
});
|
||||
client.multi(query).exec(function (err, replies) {
|
||||
log(query, t2);
|
||||
gotFilteredData(err, replies.filter(applyFilter(filter)));
|
||||
});
|
||||
|
||||
function gotFilteredData(err, nodes) {
|
||||
if (err) return callback(null);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function orderLimitStage(err, keys) {
|
||||
log(cmd, t1);
|
||||
var t2 = Date.now();
|
||||
if (err) {
|
||||
return callback(err, []);
|
||||
}
|
||||
|
||||
gotKeys(keys);
|
||||
}
|
||||
|
||||
function gotKeys(keys) {
|
||||
|
||||
// ORDER
|
||||
var reverse = false;
|
||||
if (filter && filter.order) {
|
||||
var orders = filter.order;
|
||||
if (typeof filter.order === "string"){
|
||||
orders = [filter.order];
|
||||
}
|
||||
orders.forEach(function (key) {
|
||||
var m = key.match(/\s+(A|DE)SC$/i);
|
||||
if (m) {
|
||||
key = key.replace(/\s+(A|DE)SC/i, '');
|
||||
if (m[1] === 'DE') reverse = true;
|
||||
}
|
||||
if (key !== 'id') {
|
||||
if (props[key].type.name !== 'Number' && props[key].type.name !== 'Date') {
|
||||
allNumeric = false;
|
||||
}
|
||||
}
|
||||
sortCmd.push("BY", model + ":*->" + key);
|
||||
});
|
||||
}
|
||||
|
||||
// LIMIT
|
||||
if (keys === '*' && filter && filter.limit){
|
||||
var from = (filter.offset || 0), to = from + filter.limit;
|
||||
sortCmd.push("LIMIT", from, to);
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
sortCmd.push('DESC');
|
||||
}
|
||||
|
||||
if (sortCmd.length) {
|
||||
sortCmd.unshift("s:" + model);
|
||||
sortCmd.push("GET", "#");
|
||||
cmd = "SORT " + sortCmd.join(" ");
|
||||
var ttt = Date.now();
|
||||
sortCmd.push(function(err, ids){
|
||||
if (err) {
|
||||
return callback(err, []);
|
||||
}
|
||||
log(cmd, ttt);
|
||||
var sortedKeys = ids.map(function (i) {
|
||||
return model + ":" + i;
|
||||
});
|
||||
handleKeys(err, intersect(sortedKeys, keys));
|
||||
});
|
||||
client.sort.apply(client, sortCmd);
|
||||
} else {
|
||||
// no sorting or filtering: just get all keys
|
||||
if (keys === '*') {
|
||||
cmd = 'KEYS ' + model + ':*';
|
||||
client.keys(model + ':*', handleKeys);
|
||||
} else {
|
||||
handleKeys(null, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeys(err, keys) {
|
||||
var t2 = Date.now();
|
||||
var query = keys.map(function (key) {
|
||||
return ['hgetall', key];
|
||||
});
|
||||
client.multi(query).exec(function (err, replies) {
|
||||
log(query, t2);
|
||||
// console.log('Redis time: %dms', Date.now() - ts);
|
||||
callback(err, filter ? replies.filter(applyFilter(filter)) : replies);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
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;
|
||||
var index = {};
|
||||
filteredKeys.forEach(function (x) {
|
||||
index[x] = true;
|
||||
});
|
||||
return sortedKeys.filter(function (x) {
|
||||
return index[x];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function applyFilter(filter) {
|
||||
if (typeof filter.where === 'function') {
|
||||
return filter.where;
|
||||
}
|
||||
var keys = Object.keys(filter.where || {});
|
||||
return function (obj) {
|
||||
var pass = true;
|
||||
if (!obj) return false;
|
||||
keys.forEach(function (key) {
|
||||
if (!test(filter.where[key], obj[key])) {
|
||||
pass = false;
|
||||
}
|
||||
});
|
||||
return pass;
|
||||
};
|
||||
|
||||
function test(example, value) {
|
||||
if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
|
||||
return value.match(example);
|
||||
}
|
||||
// not strict equality
|
||||
return example == value;
|
||||
}
|
||||
}
|
||||
|
||||
BridgeToRedis.prototype.destroyAll = function destroyAll(model, callback) {
|
||||
var keysQuery = model + ':*';
|
||||
var t1 = Date.now();
|
||||
this.client.keys(keysQuery, function (err, keys) {
|
||||
this.log('KEYS ' + keysQuery, t1);
|
||||
if (err) {
|
||||
return callback(err, []);
|
||||
}
|
||||
var query = keys.map(function (key) {
|
||||
return ['del', key];
|
||||
});
|
||||
var t2 = Date.now();
|
||||
this.client.multi(query).exec(function (err, replies) {
|
||||
this.log(query, t2);
|
||||
this.client.del('s:' + model, function () {
|
||||
callback(err);
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.count = function count(model, callback, where) {
|
||||
var keysQuery = model + ':*';
|
||||
var t1 = Date.now();
|
||||
if (where && Object.keys(where).length) {
|
||||
this.all(model, {where: where}, function (err, data) {
|
||||
callback(err, err ? null : data.length);
|
||||
});
|
||||
} else {
|
||||
this.client.keys(keysQuery, function (err, keys) {
|
||||
this.log('KEYS ' + keysQuery, t1);
|
||||
callback(err, err ? null : keys.length);
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
|
||||
var t1 = Date.now();
|
||||
deleteNulls(data);
|
||||
this.client.hmset(model + ':' + id, data, function () {
|
||||
this.log('HMSET ' + model + ':' + id, t1);
|
||||
this.updateIndexes(model, id, data, cb);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
function deleteNulls(data) {
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (data[key] === null) delete data[key];
|
||||
});
|
||||
}
|
||||
|
||||
BridgeToRedis.prototype.disconnect = function disconnect() {
|
||||
this.log('QUIT', Date.now());
|
||||
this.client.quit();
|
||||
};
|
||||
|
|
@ -1,556 +0,0 @@
|
|||
var safeRequire = require('../utils').safeRequire;
|
||||
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
var redis = safeRequire('redis');
|
||||
|
||||
exports.initialize = function initializeSchema(schema, callback) {
|
||||
if (!redis) return;
|
||||
|
||||
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
|
||||
);
|
||||
schema.client.auth(schema.settings.password);
|
||||
var callbackCalled = false;
|
||||
var database = schema.settings.hasOwnProperty('database') && schema.settings.database;
|
||||
schema.client.on('connect', function () {
|
||||
if (!callbackCalled && database === false) {
|
||||
callbackCalled = true;
|
||||
callback();
|
||||
} else if (database !== false) {
|
||||
if (callbackCalled) {
|
||||
return schema.client.select(schema.settings.database);
|
||||
} else {
|
||||
callbackCalled = true;
|
||||
return schema.client.select(schema.settings.database, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
schema.client.on('error', function (error) {
|
||||
console.log(error);
|
||||
})
|
||||
|
||||
var clientWrapper = new Client(schema.client);
|
||||
|
||||
schema.adapter = new BridgeToRedis(clientWrapper);
|
||||
clientWrapper._adapter = schema.adapter;
|
||||
};
|
||||
|
||||
function Client(client) {
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
var commands = Object.keys(redis.Multi.prototype).filter(function (n) {
|
||||
return n.match(/^[a-z]/);
|
||||
});
|
||||
|
||||
commands.forEach(function (cmd) {
|
||||
|
||||
Client.prototype[cmd] = function (args, callback) {
|
||||
|
||||
var c = this._client, log;
|
||||
|
||||
if (typeof args === 'string') {
|
||||
args = [args];
|
||||
}
|
||||
|
||||
if (!args) args = [];
|
||||
|
||||
log = this._adapter.logger(cmd.toUpperCase() + ' ' + args.map(function (a) {
|
||||
if (typeof a === 'object') return JSON.stringify(a);
|
||||
return a;
|
||||
}).join(' '));
|
||||
args.push(function (err, res) {
|
||||
if (err) console.log(err);
|
||||
log();
|
||||
if (callback) {
|
||||
callback(err, res);
|
||||
}
|
||||
});
|
||||
|
||||
c[cmd].apply(c, args);
|
||||
};
|
||||
});
|
||||
|
||||
Client.prototype.multi = function (commands, callback) {
|
||||
if (commands.length === 0) return callback && callback();
|
||||
if (commands.length === 1) {
|
||||
return this[commands[0].shift().toLowerCase()].call(
|
||||
this,
|
||||
commands[0],
|
||||
callback && function (e, r) { callback(e, [r]) });
|
||||
}
|
||||
var log = this._adapter.logger('MULTI\n ' + commands.map(function (x) {
|
||||
return x.join(' ');
|
||||
}).join('\n ') + '\nEXEC');
|
||||
this._client.multi(commands).exec(function (err, replies) {
|
||||
if (err) console.log(err);
|
||||
log();
|
||||
callback && callback(err, replies);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.transaction = function () {
|
||||
return new Transaction(this);
|
||||
};
|
||||
|
||||
function Transaction(client) {
|
||||
this._client = client;
|
||||
this._handlers = [];
|
||||
this._schedule = [];
|
||||
}
|
||||
|
||||
Transaction.prototype.run = function (cb) {
|
||||
var t = this;
|
||||
var atLeastOneHandler = false;
|
||||
switch (this._schedule.length) {
|
||||
case 0: return cb();
|
||||
case 1: return this._client[this._schedule[0].shift()].call(
|
||||
this._client,
|
||||
this._schedule[0],
|
||||
this._handlers[0] || cb);
|
||||
default:
|
||||
this._client.multi(this._schedule, function (err, replies) {
|
||||
if (err) return cb(err);
|
||||
replies.forEach(function (r, i) {
|
||||
if (t._handlers[i]) {
|
||||
atLeastOneHandler = true;
|
||||
t._handlers[i](err, r);
|
||||
}
|
||||
});
|
||||
if (!atLeastOneHandler) cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
commands.forEach(function (k) {
|
||||
|
||||
Transaction.prototype[k] = function (args, cb) {
|
||||
if (typeof args === 'string') {
|
||||
args = [args];
|
||||
}
|
||||
args.unshift(k);
|
||||
this._schedule.push(args);
|
||||
this._handlers.push(cb || false);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
function BridgeToRedis(client) {
|
||||
this._models = {};
|
||||
this.client = client;
|
||||
this.indexes = {};
|
||||
}
|
||||
|
||||
BridgeToRedis.prototype.define = function (descr) {
|
||||
var m = descr.model.modelName;
|
||||
this._models[m] = descr;
|
||||
this.indexes[m] = {};
|
||||
Object.keys(descr.properties).forEach(function (prop) {
|
||||
if (descr.properties[prop].index) {
|
||||
this.indexes[m][prop] = descr.properties[prop].type;
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.defineForeignKey = function (model, key, cb) {
|
||||
this.indexes[model][key] = Number;
|
||||
cb(null, Number);
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.forDb = function (model, data) {
|
||||
var p = this._models[model].properties;
|
||||
for (var i in data) {
|
||||
if (!p[i]) continue;
|
||||
if (!data[i]) {
|
||||
data[i] = "";
|
||||
continue;
|
||||
}
|
||||
switch (p[i].type.name) {
|
||||
case "Date":
|
||||
data[i] = data[i].getTime ? data[i].getTime().toString() : "0";
|
||||
break;
|
||||
case "Number":
|
||||
data[i] = data[i].toString();
|
||||
break;
|
||||
case "Boolean":
|
||||
data[i] = !!data[i] ? "1" : "0";
|
||||
break;
|
||||
case "String":
|
||||
case "Text":
|
||||
break;
|
||||
default:
|
||||
data[i] = JSON.stringify(data[i]);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.fromDb = function (model, data) {
|
||||
var p = this._models[model].properties, d;
|
||||
for (var i in data) {
|
||||
if (!p[i]) continue;
|
||||
if (!data[i]) {
|
||||
data[i] = "";
|
||||
continue;
|
||||
}
|
||||
switch (p[i].type.name) {
|
||||
case "Date":
|
||||
d = new Date(data[i]);
|
||||
d.setTime(data[i]);
|
||||
data[i] = d;
|
||||
break;
|
||||
case "Number":
|
||||
data[i] = Number(data[i]);
|
||||
break;
|
||||
case "Boolean":
|
||||
data[i] = data[i] === "1";
|
||||
break;
|
||||
default:
|
||||
d = data[i];
|
||||
try {
|
||||
data[i] = JSON.parse(data[i]);
|
||||
}
|
||||
catch(e) {
|
||||
data[i] = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.save = function (model, data, callback) {
|
||||
data = this.forDb(model, data);
|
||||
deleteNulls(data);
|
||||
this.client.hgetall(model + ':' + data.id, function (err, prevData) {
|
||||
if (err) return callback(err);
|
||||
this.client.hmset([model + ':' + data.id, data], function (err) {
|
||||
if (err) return callback(err);
|
||||
if (prevData) {
|
||||
Object.keys(prevData).forEach(function (k) {
|
||||
if (data.hasOwnProperty(k)) return;
|
||||
data[k] = prevData[k];
|
||||
});
|
||||
}
|
||||
this.updateIndexes(model, data.id, data, callback, this.forDb(model, prevData));
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback, prevData) {
|
||||
var p = this._models[model].properties;
|
||||
var i = this.indexes[model];
|
||||
var schedule = [];
|
||||
if (!callback.removed) {
|
||||
schedule.push(['SADD', 's:' + model, id]);
|
||||
}
|
||||
Object.keys(i).forEach(function (key) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
var val = data[key];
|
||||
schedule.push([
|
||||
'SADD',
|
||||
'i:' + model + ':' + key + ':' + val,
|
||||
id
|
||||
]);
|
||||
}
|
||||
if (prevData && prevData[key] !== data[key]) {
|
||||
schedule.push([
|
||||
'SREM',
|
||||
'i:' + model + ':' + key + ':' + prevData[key],
|
||||
id
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
if (schedule.length) {
|
||||
this.client.multi(schedule, function (err) {
|
||||
callback(err, data);
|
||||
});
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.create = function (model, data, callback) {
|
||||
if (data.id) return create.call(this, data.id, true);
|
||||
|
||||
this.client.incr('id:' + model, function (err, id) {
|
||||
create.call(this, id);
|
||||
}.bind(this));
|
||||
|
||||
function create(id, upsert) {
|
||||
data.id = id.toString();
|
||||
this.save(model, data, function (err) {
|
||||
if (callback) {
|
||||
callback(err, parseInt(id, 10));
|
||||
}
|
||||
});
|
||||
|
||||
// push the id to the list of user ids for sorting
|
||||
this.client.sadd(['s:' + model, data.id]);
|
||||
}
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.updateOrCreate = function (model, data, callback) {
|
||||
if (!data.id) {
|
||||
return this.create(model, data, callback);
|
||||
}
|
||||
var client = this.client;
|
||||
this.save(model, data, function (error, obj) {
|
||||
var key = 'id:' + model;
|
||||
client.get(key, function (err, id) {
|
||||
if (!id || data.id > parseInt(id, 10)) {
|
||||
client.set([key, data.id], ok);
|
||||
} else {
|
||||
ok();
|
||||
}
|
||||
});
|
||||
function ok() {
|
||||
callback(error, obj);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.exists = function (model, id, callback) {
|
||||
this.client.exists(model + ':' + id, function (err, exists) {
|
||||
if (callback) {
|
||||
callback(err, exists);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.find = function find(model, id, callback) {
|
||||
this.client.hgetall(model + ':' + id, function (err, data) {
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
data.id = id;
|
||||
} else {
|
||||
data = null;
|
||||
}
|
||||
callback(err, this.fromDb(model, data));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.destroy = function destroy(model, id, callback) {
|
||||
var br = this;
|
||||
var tr = br.client.transaction();
|
||||
|
||||
br.client.hgetall(model + ':' + id, function (err, data) {
|
||||
if (err) return callback(err);
|
||||
|
||||
tr.srem(['s:' + model, id]);
|
||||
tr.del(model + ':' + id);
|
||||
tr.run(function (err) {
|
||||
if (err) return callback(err);
|
||||
callback.removed = true;
|
||||
|
||||
br.updateIndexes(model, id, {}, callback, data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.possibleIndexes = function (model, filter) {
|
||||
if (!filter || Object.keys(filter.where || {}).length === 0) return false;
|
||||
|
||||
var foundIndex = [];
|
||||
var noIndex = [];
|
||||
Object.keys(filter.where).forEach(function (key) {
|
||||
var i = this.indexes[model][key];
|
||||
if (i) {
|
||||
var val = filter.where[key];
|
||||
if (i.name === 'Date') {
|
||||
val = val && val.getTime ? val.getTime() : 0;
|
||||
}
|
||||
foundIndex.push('i:' + model + ':' + key + ':' + val);
|
||||
} else {
|
||||
noIndex.push(key);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
return [foundIndex, noIndex];
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.all = function all(model, filter, callback) {
|
||||
var ts = Date.now();
|
||||
var client = this.client;
|
||||
var cmd;
|
||||
var redis = this;
|
||||
var sortCmd = [];
|
||||
var props = this._models[model].properties;
|
||||
var allNumeric = true;
|
||||
var dest = 'temp' + (Date.now() * Math.random());
|
||||
var innerSetUsed = false;
|
||||
var trans = this.client.transaction();
|
||||
|
||||
if (!filter) {
|
||||
filter = {order: 'id'};
|
||||
}
|
||||
|
||||
// WHERE
|
||||
if (filter.where) {
|
||||
var pi = this.possibleIndexes(model, filter);
|
||||
var indexes = pi[0];
|
||||
var noIndexes = pi[1];
|
||||
|
||||
if (noIndexes.length) {
|
||||
throw new Error(model + ': no indexes found for ' +
|
||||
noIndexes.join(', ') +
|
||||
' impossible to sort and filter using redis adapter');
|
||||
}
|
||||
|
||||
if (indexes && indexes.length) {
|
||||
innerSetUsed = true;
|
||||
if (indexes.length > 1) {
|
||||
indexes.unshift(dest);
|
||||
trans.sinterstore(indexes);
|
||||
} else {
|
||||
dest = indexes[0];
|
||||
}
|
||||
} else {
|
||||
throw new Error('No indexes for ' + filter.where);
|
||||
}
|
||||
} else {
|
||||
dest = 's:' + model;
|
||||
// no filtering, just sort/limit (if any)
|
||||
}
|
||||
|
||||
// only counting?
|
||||
if (filter.getCount) {
|
||||
trans.scard(dest, callback);
|
||||
return trans.run();
|
||||
}
|
||||
|
||||
// ORDER
|
||||
var reverse = false;
|
||||
if (!filter.order) {
|
||||
filter.order = 'id';
|
||||
}
|
||||
var orders = filter.order;
|
||||
if (typeof filter.order === "string"){
|
||||
orders = [filter.order];
|
||||
}
|
||||
|
||||
orders.forEach(function (key) {
|
||||
var m = key.match(/\s+(A|DE)SC$/i);
|
||||
if (m) {
|
||||
key = key.replace(/\s+(A|DE)SC/i, '');
|
||||
if (m[1] === 'DE') reverse = true;
|
||||
}
|
||||
if (key !== 'id') {
|
||||
if (props[key].type.name !== 'Number' && props[key].type.name !== 'Date') {
|
||||
allNumeric = false;
|
||||
}
|
||||
}
|
||||
sortCmd.push("BY", model + ":*->" + key);
|
||||
});
|
||||
|
||||
// LIMIT
|
||||
if (filter.limit){
|
||||
var offset = (filter.offset || 0), limit = filter.limit;
|
||||
sortCmd.push("LIMIT", offset, limit);
|
||||
}
|
||||
|
||||
// we need ALPHA modifier when sorting string values
|
||||
// the only case it's not required - we sort numbers
|
||||
if (!allNumeric) {
|
||||
sortCmd.push('ALPHA');
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
sortCmd.push('DESC');
|
||||
}
|
||||
|
||||
sortCmd.unshift(dest);
|
||||
sortCmd.push("GET", "#");
|
||||
cmd = "SORT " + sortCmd.join(" ");
|
||||
var ttt = Date.now();
|
||||
trans.sort(sortCmd, function (err, ids){
|
||||
if (err) {
|
||||
return callback(err, []);
|
||||
}
|
||||
var sortedKeys = ids.map(function (i) {
|
||||
return model + ":" + i;
|
||||
});
|
||||
handleKeys(err, sortedKeys);
|
||||
});
|
||||
|
||||
if (dest.match(/^temp/)) {
|
||||
trans.del(dest);
|
||||
}
|
||||
|
||||
trans.run(callback);
|
||||
|
||||
function handleKeys(err, keys) {
|
||||
var t2 = Date.now();
|
||||
var query = keys.map(function (key) {
|
||||
return ['hgetall', key];
|
||||
});
|
||||
client.multi(query, function (err, replies) {
|
||||
callback(err, (replies || []).map(function (r) {
|
||||
return redis.fromDb(model, r);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
function numerically(a, b) {
|
||||
return a[this[0]] - b[this[0]];
|
||||
}
|
||||
|
||||
function literally(a, b) {
|
||||
return a[this[0]] > b[this[0]];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.destroyAll = function destroyAll(model, callback) {
|
||||
var br = this;
|
||||
this.client.multi([
|
||||
['KEYS', model + ':*'],
|
||||
['KEYS', '*:' + model + ':*']
|
||||
], function (err, k) {
|
||||
br.client.del(k[0].concat(k[1]).concat('s:' + model), callback);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.count = function count(model, callback, where) {
|
||||
if (where && Object.keys(where).length) {
|
||||
this.all(model, {where: where, getCount: true}, callback);
|
||||
} else {
|
||||
this.client.scard('s:' + model, callback);
|
||||
}
|
||||
};
|
||||
|
||||
BridgeToRedis.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
|
||||
data.id = id;
|
||||
this.save(model, data, cb);
|
||||
};
|
||||
|
||||
function deleteNulls(data) {
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (data[key] === null) delete data[key];
|
||||
});
|
||||
}
|
||||
|
||||
BridgeToRedis.prototype.disconnect = function disconnect() {
|
||||
this.client.quit();
|
||||
};
|
||||
|
|
@ -62,9 +62,14 @@ function Schema(name, settings) {
|
|||
// this is only one initialization entry point of adapter
|
||||
// this module should define `adapter` member of `this` (schema)
|
||||
var adapter;
|
||||
if (existsSync(__dirname + '/adapters/' + name + '.js')) {
|
||||
if (name.match(/^\//)) {
|
||||
// try absolute path
|
||||
adapter = require(name);
|
||||
} else if (existsSync(__dirname + '/adapters/' + name + '.js')) {
|
||||
// try built-in adapter
|
||||
adapter = require('./adapters/' + name);
|
||||
} else {
|
||||
// try foreign adapter
|
||||
try {
|
||||
adapter = require('jugglingdb-' + name);
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "jugglingdb",
|
||||
"description": "ORM for every database: redis, mysql, neo4j, mongodb, couchdb, postgres, sqlite",
|
||||
"version": "0.1.27-3",
|
||||
"version": "0.2.0",
|
||||
"author": "Anatoliy Chakkaev <rpm1602@gmail.com>",
|
||||
"contributors": [
|
||||
{
|
||||
|
@ -51,7 +51,7 @@
|
|||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "EXCEPT=cradle,neo4j nodeunit test/*_test*"
|
||||
"test": "EXCEPT=cradle,neo4j,nano nodeunit test/*_test*"
|
||||
},
|
||||
"engines": [
|
||||
"node >= 0.4.12"
|
||||
|
|
|
@ -1,63 +1,53 @@
|
|||
require('./spec_helper').init(exports);
|
||||
|
||||
var Schema = require('../index').Schema;
|
||||
var Text = Schema.Text;
|
||||
|
||||
var schemas = {
|
||||
// riak: {},
|
||||
mysql: {
|
||||
database: 'myapp_test',
|
||||
username: 'root'
|
||||
},
|
||||
postgres: {
|
||||
database: 'myapp_test',
|
||||
username: 'postgres'
|
||||
},
|
||||
sqlite3: {
|
||||
database: ':memory:'
|
||||
},
|
||||
neo4j: { url: 'http://localhost:7474/' },
|
||||
// mongoose: { url: 'mongodb://travis:test@localhost:27017/myapp' },
|
||||
mongodb: { url: 'mongodb://travis:test@localhost:27017/myapp' },
|
||||
redis2: {},
|
||||
memory: {},
|
||||
cradle: {},
|
||||
nano: { url: 'http://localhost:5984/nano-test' }
|
||||
};
|
||||
// var schemas = {
|
||||
// // riak: {},
|
||||
// mysql: {
|
||||
// database: 'myapp_test',
|
||||
// username: 'root'
|
||||
// },
|
||||
// postgres: {
|
||||
// database: 'myapp_test',
|
||||
// username: 'postgres'
|
||||
// },
|
||||
// sqlite3: {
|
||||
// database: ':memory:'
|
||||
// },
|
||||
// neo4j: { url: 'http://localhost:7474/' },
|
||||
// // mongoose: { url: 'mongodb://travis:test@localhost:27017/myapp' },
|
||||
// mongodb: { url: 'mongodb://travis:test@localhost:27017/myapp' },
|
||||
// redis2: {},
|
||||
// memory: {},
|
||||
// cradle: {},
|
||||
// nano: { url: 'http://localhost:5984/nano-test' }
|
||||
// };
|
||||
|
||||
var specificTest = getSpecificTests();
|
||||
var testPerformed = false;
|
||||
var nbSchemaRequests = 0;
|
||||
|
||||
Object.keys(schemas).forEach(function (schemaName) {
|
||||
if (process.env.ONLY && process.env.ONLY !== schemaName) return;
|
||||
if (process.env.EXCEPT && ~process.env.EXCEPT.indexOf(schemaName)) return;
|
||||
performTestFor(schemaName);
|
||||
});
|
||||
var batch;
|
||||
|
||||
if (process.env.ONLY && !testPerformed) {
|
||||
performTestFor(process.env.ONLY);
|
||||
function it(name, cases) {
|
||||
batch[name] = cases;
|
||||
}
|
||||
|
||||
function performTestFor(schemaName) {
|
||||
testPerformed = true;
|
||||
context(schemaName, function () {
|
||||
var schema = new Schema(schemaName, schemas[schemaName] || {});
|
||||
module.exports = function testSchema(exportCasesHere, schema) {
|
||||
|
||||
it('should connect to database', function (test) {
|
||||
if (schema.connected) return test.done();
|
||||
schema.on('connected', test.done);
|
||||
});
|
||||
batch = exportCasesHere;
|
||||
|
||||
schema.log = function (a) {
|
||||
console.log(a);
|
||||
nbSchemaRequests++;
|
||||
};
|
||||
|
||||
testOrm(schema);
|
||||
if (specificTest[schemaName]) specificTest[schemaName](schema);
|
||||
it('should connect to database', function (test) {
|
||||
if (schema.connected) return test.done();
|
||||
schema.on('connected', test.done);
|
||||
});
|
||||
}
|
||||
|
||||
schema.log = function (a) {
|
||||
console.log(a);
|
||||
nbSchemaRequests++;
|
||||
};
|
||||
|
||||
testOrm(schema);
|
||||
};
|
||||
|
||||
function testOrm(schema) {
|
||||
var requestsAreCounted = schema.name !== 'mongodb';
|
||||
|
@ -454,108 +444,108 @@ function testOrm(schema) {
|
|||
});
|
||||
|
||||
|
||||
if (
|
||||
!schema.name.match(/redis/) &&
|
||||
schema.name !== 'memory' &&
|
||||
schema.name !== 'neo4j' &&
|
||||
schema.name !== 'cradle' &&
|
||||
schema.name !== 'nano' &&
|
||||
schema.name !== 'mongodb'
|
||||
)
|
||||
it('hasMany should support additional conditions', function (test) {
|
||||
// if (
|
||||
// !schema.name.match(/redis/) &&
|
||||
// schema.name !== 'memory' &&
|
||||
// schema.name !== 'neo4j' &&
|
||||
// schema.name !== 'cradle' &&
|
||||
// schema.name !== 'nano' &&
|
||||
// schema.name !== 'mongodb'
|
||||
// )
|
||||
// it('hasMany should support additional conditions', function (test) {
|
||||
|
||||
// Finding one post with an existing author associated
|
||||
Post.all(function (err, posts) {
|
||||
// We try to get the first post with a userId != NULL
|
||||
for (var i = 0; i < posts.length; i++) {
|
||||
var post = posts[i];
|
||||
if (post.userId !== null) {
|
||||
// We could get the user with belongs to relationship but it is better if there is no interactions.
|
||||
User.find(post.userId, function(err, user) {
|
||||
user.posts({where: {id: post.id}}, function(err, posts) {
|
||||
test.equal(posts.length, 1, 'There should be only 1 post.');
|
||||
test.done();
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// // Finding one post with an existing author associated
|
||||
// Post.all(function (err, posts) {
|
||||
// // We try to get the first post with a userId != NULL
|
||||
// for (var i = 0; i < posts.length; i++) {
|
||||
// var post = posts[i];
|
||||
// if (post.userId !== null) {
|
||||
// // We could get the user with belongs to relationship but it is better if there is no interactions.
|
||||
// User.find(post.userId, function(err, user) {
|
||||
// user.posts({where: {id: post.id}}, function(err, posts) {
|
||||
// test.equal(posts.length, 1, 'There should be only 1 post.');
|
||||
// test.done();
|
||||
// });
|
||||
// });
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
if (
|
||||
!schema.name.match(/redis/) &&
|
||||
schema.name !== 'memory' &&
|
||||
schema.name !== 'neo4j' &&
|
||||
schema.name !== 'cradle' &&
|
||||
schema.name !== 'nano'
|
||||
)
|
||||
it('hasMany should be cached', function (test) {
|
||||
// Finding one post with an existing author associated
|
||||
Post.all(function (err, posts) {
|
||||
// We try to get the first post with a userId != NULL
|
||||
for (var i = 0; i < posts.length; i++) {
|
||||
var post = posts[i];
|
||||
if (post.userId !== null) {
|
||||
// We could get the user with belongs to relationship but it is better if there is no interactions.
|
||||
User.find(post.userId, function(err, user) {
|
||||
User.create(function(err, voidUser) {
|
||||
Post.create({userId: user.id}, function() {
|
||||
// if (
|
||||
// !schema.name.match(/redis/) &&
|
||||
// schema.name !== 'memory' &&
|
||||
// schema.name !== 'neo4j' &&
|
||||
// schema.name !== 'cradle' &&
|
||||
// schema.name !== 'nano'
|
||||
// )
|
||||
// it('hasMany should be cached', function (test) {
|
||||
// // Finding one post with an existing author associated
|
||||
// Post.all(function (err, posts) {
|
||||
// // We try to get the first post with a userId != NULL
|
||||
// for (var i = 0; i < posts.length; i++) {
|
||||
// var post = posts[i];
|
||||
// if (post.userId !== null) {
|
||||
// // We could get the user with belongs to relationship but it is better if there is no interactions.
|
||||
// User.find(post.userId, function(err, user) {
|
||||
// User.create(function(err, voidUser) {
|
||||
// Post.create({userId: user.id}, function() {
|
||||
|
||||
// There can't be any concurrency because we are counting requests
|
||||
// We are first testing cases when user has posts
|
||||
user.posts(function(err, data) {
|
||||
var nbInitialRequests = nbSchemaRequests;
|
||||
user.posts(function(err, data2) {
|
||||
test.equal(data.length, 2, 'There should be 2 posts.');
|
||||
test.equal(data.length, data2.length, 'Posts should be the same, since we are loading on the same object.');
|
||||
requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
// // There can't be any concurrency because we are counting requests
|
||||
// // We are first testing cases when user has posts
|
||||
// user.posts(function(err, data) {
|
||||
// var nbInitialRequests = nbSchemaRequests;
|
||||
// user.posts(function(err, data2) {
|
||||
// test.equal(data.length, 2, 'There should be 2 posts.');
|
||||
// test.equal(data.length, data2.length, 'Posts should be the same, since we are loading on the same object.');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
|
||||
if (schema.name === 'mongodb') { // for the moment mongodb doesn\'t support additional conditions on hasMany relations (see above)
|
||||
test.done();
|
||||
} else {
|
||||
user.posts({where: {id: data[0].id}}, function(err, data) {
|
||||
test.equal(data.length, 1, 'There should be only one post.');
|
||||
requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should be one additional request since we added conditions.');
|
||||
// if (schema.name === 'mongodb') { // for the moment mongodb doesn\'t support additional conditions on hasMany relations (see above)
|
||||
// test.done();
|
||||
// } else {
|
||||
// user.posts({where: {id: data[0].id}}, function(err, data) {
|
||||
// test.equal(data.length, 1, 'There should be only one post.');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should be one additional request since we added conditions.');
|
||||
|
||||
user.posts(function(err, data) {
|
||||
test.equal(data.length, 2, 'Previous get shouldn\'t have changed cached value though, since there was additional conditions.');
|
||||
requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
// user.posts(function(err, data) {
|
||||
// test.equal(data.length, 2, 'Previous get shouldn\'t have changed cached value though, since there was additional conditions.');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
|
||||
// We are now testing cases when user doesn't have any post
|
||||
voidUser.posts(function(err, data) {
|
||||
var nbInitialRequests = nbSchemaRequests;
|
||||
voidUser.posts(function(err, data2) {
|
||||
test.equal(data.length, 0, 'There shouldn\'t be any posts (1/2).');
|
||||
test.equal(data2.length, 0, 'There shouldn\'t be any posts (2/2).');
|
||||
requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
// // We are now testing cases when user doesn't have any post
|
||||
// voidUser.posts(function(err, data) {
|
||||
// var nbInitialRequests = nbSchemaRequests;
|
||||
// voidUser.posts(function(err, data2) {
|
||||
// test.equal(data.length, 0, 'There shouldn\'t be any posts (1/2).');
|
||||
// test.equal(data2.length, 0, 'There shouldn\'t be any posts (2/2).');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
|
||||
voidUser.posts(true, function(err, data3) {
|
||||
test.equal(data3.length, 0, 'There shouldn\'t be any posts.');
|
||||
requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should be one additional request since we forced refresh.');
|
||||
// voidUser.posts(true, function(err, data3) {
|
||||
// test.equal(data3.length, 0, 'There shouldn\'t be any posts.');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should be one additional request since we forced refresh.');
|
||||
|
||||
test.done();
|
||||
});
|
||||
});
|
||||
});
|
||||
// test.done();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
});
|
||||
});
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
});
|
||||
// });
|
||||
|
||||
// it('should handle hasOne relationship', function (test) {
|
||||
// User.create(function (err, u) {
|
||||
|
@ -760,144 +750,144 @@ function testOrm(schema) {
|
|||
|
||||
});
|
||||
|
||||
if (
|
||||
!schema.name.match(/redis/) &&
|
||||
schema.name !== 'memory' &&
|
||||
schema.name !== 'neo4j' &&
|
||||
schema.name !== 'cradle' &&
|
||||
schema.name !== 'nano'
|
||||
)
|
||||
it('should allow advanced queying: lt, gt, lte, gte, between', function (test) {
|
||||
Post.destroyAll(function () {
|
||||
Post.create({date: new Date('Wed, 01 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Thu, 02 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Fri, 03 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Sat, 04 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Sun, 05 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Mon, 06 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Wed, 08 Feb 2012 13:56:12 GMT')}, done);
|
||||
Post.create({date: new Date('Thu, 09 Feb 2012 13:56:12 GMT')}, done);
|
||||
});
|
||||
// if (
|
||||
// !schema.name.match(/redis/) &&
|
||||
// schema.name !== 'memory' &&
|
||||
// schema.name !== 'neo4j' &&
|
||||
// schema.name !== 'cradle' &&
|
||||
// schema.name !== 'nano'
|
||||
// )
|
||||
// it('should allow advanced queying: lt, gt, lte, gte, between', function (test) {
|
||||
// Post.destroyAll(function () {
|
||||
// Post.create({date: new Date('Wed, 01 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Thu, 02 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Fri, 03 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Sat, 04 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Sun, 05 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Mon, 06 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Wed, 08 Feb 2012 13:56:12 GMT')}, done);
|
||||
// Post.create({date: new Date('Thu, 09 Feb 2012 13:56:12 GMT')}, done);
|
||||
// });
|
||||
|
||||
var posts = 9;
|
||||
function done() {
|
||||
if (--posts === 0) makeTest();
|
||||
}
|
||||
// var posts = 9;
|
||||
// function done() {
|
||||
// if (--posts === 0) makeTest();
|
||||
// }
|
||||
|
||||
function makeTest() {
|
||||
// gt
|
||||
Post.all({where: {date: {gt: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
test.equal(posts.length, 2, 'gt');
|
||||
ok();
|
||||
});
|
||||
// function makeTest() {
|
||||
// // gt
|
||||
// Post.all({where: {date: {gt: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
// test.equal(posts.length, 2, 'gt');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// gte
|
||||
Post.all({where: {date: {gte: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
test.equal(posts.length, 3, 'gte');
|
||||
ok();
|
||||
});
|
||||
// // gte
|
||||
// Post.all({where: {date: {gte: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
// test.equal(posts.length, 3, 'gte');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// lte
|
||||
Post.all({where: {date: {lte: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
test.equal(posts.length, 7, 'lte');
|
||||
ok();
|
||||
});
|
||||
// // lte
|
||||
// Post.all({where: {date: {lte: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
// test.equal(posts.length, 7, 'lte');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// lt
|
||||
Post.all({where: {date: {lt: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
test.equal(posts.length, 6, 'lt');
|
||||
ok();
|
||||
});
|
||||
// // lt
|
||||
// Post.all({where: {date: {lt: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
|
||||
// test.equal(posts.length, 6, 'lt');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// between
|
||||
Post.all({where: {date: {between: [new Date('Tue, 05 Feb 2012 13:56:12 GMT'), new Date('Tue, 09 Feb 2012 13:56:12 GMT')]}}}, function (err, posts) {
|
||||
test.equal(posts.length, 5, 'between');
|
||||
ok();
|
||||
});
|
||||
}
|
||||
// // between
|
||||
// Post.all({where: {date: {between: [new Date('Tue, 05 Feb 2012 13:56:12 GMT'), new Date('Tue, 09 Feb 2012 13:56:12 GMT')]}}}, function (err, posts) {
|
||||
// test.equal(posts.length, 5, 'between');
|
||||
// ok();
|
||||
// });
|
||||
// }
|
||||
|
||||
var tests = 5;
|
||||
function ok() {
|
||||
if (--tests === 0) test.done();
|
||||
}
|
||||
});
|
||||
// var tests = 5;
|
||||
// function ok() {
|
||||
// if (--tests === 0) test.done();
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
if (
|
||||
schema.name === 'mysql' ||
|
||||
schema.name === 'postgres'
|
||||
)
|
||||
it('should allow IN or NOT IN', function (test) {
|
||||
User.destroyAll(function () {
|
||||
User.create({name: 'User A', age: 21}, done);
|
||||
User.create({name: 'User B', age: 22}, done);
|
||||
User.create({name: 'User C', age: 23}, done);
|
||||
User.create({name: 'User D', age: 24}, done);
|
||||
User.create({name: 'User E', age: 25}, done);
|
||||
});
|
||||
// if (
|
||||
// schema.name === 'mysql' ||
|
||||
// schema.name === 'postgres'
|
||||
// )
|
||||
// it('should allow IN or NOT IN', function (test) {
|
||||
// User.destroyAll(function () {
|
||||
// User.create({name: 'User A', age: 21}, done);
|
||||
// User.create({name: 'User B', age: 22}, done);
|
||||
// User.create({name: 'User C', age: 23}, done);
|
||||
// User.create({name: 'User D', age: 24}, done);
|
||||
// User.create({name: 'User E', age: 25}, done);
|
||||
// });
|
||||
|
||||
var users = 5;
|
||||
function done() {
|
||||
if (--users === 0) makeTest();
|
||||
}
|
||||
// var users = 5;
|
||||
// function done() {
|
||||
// if (--users === 0) makeTest();
|
||||
// }
|
||||
|
||||
function makeTest() {
|
||||
// IN with empty array should return nothing
|
||||
User.all({where: {name: {inq: []}}}, function (err, users) {
|
||||
test.equal(users.length, 0, 'IN with empty array returns nothing');
|
||||
ok();
|
||||
});
|
||||
// function makeTest() {
|
||||
// // IN with empty array should return nothing
|
||||
// User.all({where: {name: {inq: []}}}, function (err, users) {
|
||||
// test.equal(users.length, 0, 'IN with empty array returns nothing');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// NOT IN with empty array should return everything
|
||||
User.all({where: {name: {nin: []}}}, function (err, users) {
|
||||
test.equal(users.length, 5, 'NOT IN with empty array returns everything');
|
||||
ok();
|
||||
});
|
||||
// // NOT IN with empty array should return everything
|
||||
// User.all({where: {name: {nin: []}}}, function (err, users) {
|
||||
// test.equal(users.length, 5, 'NOT IN with empty array returns everything');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// IN [User A] returns user with name = User A
|
||||
User.all({where: {name: {inq: ['User A']}}}, function (err, users) {
|
||||
test.equal(users.length, 1, 'IN searching one existing value returns 1 user');
|
||||
test.equal(users[0].name, 'User A', 'IN [User A] returns user with name = User A');
|
||||
ok();
|
||||
});
|
||||
// // IN [User A] returns user with name = User A
|
||||
// User.all({where: {name: {inq: ['User A']}}}, function (err, users) {
|
||||
// test.equal(users.length, 1, 'IN searching one existing value returns 1 user');
|
||||
// test.equal(users[0].name, 'User A', 'IN [User A] returns user with name = User A');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// NOT IN [User A] returns users with name != User A
|
||||
User.all({where: {name: {nin: ['User A']}}}, function (err, users) {
|
||||
test.equal(users.length, 4, 'IN [User A] returns users with name != User A');
|
||||
ok();
|
||||
});
|
||||
// // NOT IN [User A] returns users with name != User A
|
||||
// User.all({where: {name: {nin: ['User A']}}}, function (err, users) {
|
||||
// test.equal(users.length, 4, 'IN [User A] returns users with name != User A');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// IN [User A, User B] returns users with name = User A OR name = User B
|
||||
User.all({where: {name: {inq: ['User A', 'User B']}}}, function (err, users) {
|
||||
test.equal(users.length, 2, 'IN searching two existing values returns 2 users');
|
||||
ok();
|
||||
});
|
||||
// // IN [User A, User B] returns users with name = User A OR name = User B
|
||||
// User.all({where: {name: {inq: ['User A', 'User B']}}}, function (err, users) {
|
||||
// test.equal(users.length, 2, 'IN searching two existing values returns 2 users');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// NOT IN [User A, User B] returns users with name != User A AND name != User B
|
||||
User.all({where: {name: {nin: ['User A', 'User B']}}}, function (err, users) {
|
||||
test.equal(users.length, 3, 'NOT IN searching two existing values returns users with name != User A AND name != User B');
|
||||
ok();
|
||||
});
|
||||
// // NOT IN [User A, User B] returns users with name != User A AND name != User B
|
||||
// User.all({where: {name: {nin: ['User A', 'User B']}}}, function (err, users) {
|
||||
// test.equal(users.length, 3, 'NOT IN searching two existing values returns users with name != User A AND name != User B');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// IN works with numbers too
|
||||
User.all({where: {age: {inq: [21, 22]}}}, function (err, users) {
|
||||
test.equal(users.length, 2, 'IN works with numbers too');
|
||||
ok();
|
||||
});
|
||||
// // IN works with numbers too
|
||||
// User.all({where: {age: {inq: [21, 22]}}}, function (err, users) {
|
||||
// test.equal(users.length, 2, 'IN works with numbers too');
|
||||
// ok();
|
||||
// });
|
||||
|
||||
// NOT IN works with numbers too
|
||||
User.all({where: {age: {nin: [21, 22]}}}, function (err, users) {
|
||||
test.equal(users.length, 3, 'NOT IN works with numbers too');
|
||||
ok();
|
||||
});
|
||||
}
|
||||
// // NOT IN works with numbers too
|
||||
// User.all({where: {age: {nin: [21, 22]}}}, function (err, users) {
|
||||
// test.equal(users.length, 3, 'NOT IN works with numbers too');
|
||||
// ok();
|
||||
// });
|
||||
// }
|
||||
|
||||
var tests = 8;
|
||||
function ok() {
|
||||
if (--tests === 0) test.done();
|
||||
}
|
||||
});
|
||||
// var tests = 8;
|
||||
// function ok() {
|
||||
// if (--tests === 0) test.done();
|
||||
// }
|
||||
// });
|
||||
|
||||
it('should handle order clause with direction', function (test) {
|
||||
var wait = 0;
|
||||
|
@ -994,54 +984,54 @@ function testOrm(schema) {
|
|||
});
|
||||
});
|
||||
|
||||
if (
|
||||
!schema.name.match(/redis/) &&
|
||||
schema.name !== 'memory' &&
|
||||
schema.name !== 'neo4j' &&
|
||||
schema.name !== 'cradle' &&
|
||||
schema.name !== 'nano'
|
||||
)
|
||||
it('belongsTo should be cached', function (test) {
|
||||
User.findOne(function(err, user) {
|
||||
// if (
|
||||
// !schema.name.match(/redis/) &&
|
||||
// schema.name !== 'memory' &&
|
||||
// schema.name !== 'neo4j' &&
|
||||
// schema.name !== 'cradle' &&
|
||||
// schema.name !== 'nano'
|
||||
// )
|
||||
// it('belongsTo should be cached', function (test) {
|
||||
// User.findOne(function(err, user) {
|
||||
|
||||
var passport = new Passport({ownerId: user.id});
|
||||
var passport2 = new Passport({ownerId: null});
|
||||
// var passport = new Passport({ownerId: user.id});
|
||||
// var passport2 = new Passport({ownerId: null});
|
||||
|
||||
// There can't be any concurrency because we are counting requests
|
||||
// We are first testing cases when passport has an owner
|
||||
passport.owner(function(err, data) {
|
||||
var nbInitialRequests = nbSchemaRequests;
|
||||
passport.owner(function(err, data2) {
|
||||
test.equal(data.id, data2.id, 'The value should remain the same');
|
||||
requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
// // There can't be any concurrency because we are counting requests
|
||||
// // We are first testing cases when passport has an owner
|
||||
// passport.owner(function(err, data) {
|
||||
// var nbInitialRequests = nbSchemaRequests;
|
||||
// passport.owner(function(err, data2) {
|
||||
// test.equal(data.id, data2.id, 'The value should remain the same');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
|
||||
// We are now testing cases when passport has not an owner
|
||||
passport2.owner(function(err, data) {
|
||||
var nbInitialRequests2 = nbSchemaRequests;
|
||||
passport2.owner(function(err, data2) {
|
||||
test.equal(data, null, 'The value should be null since there is no owner');
|
||||
test.equal(data, data2, 'The value should remain the same (null)');
|
||||
requestsAreCounted && test.equal(nbInitialRequests2, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
// // We are now testing cases when passport has not an owner
|
||||
// passport2.owner(function(err, data) {
|
||||
// var nbInitialRequests2 = nbSchemaRequests;
|
||||
// passport2.owner(function(err, data2) {
|
||||
// test.equal(data, null, 'The value should be null since there is no owner');
|
||||
// test.equal(data, data2, 'The value should remain the same (null)');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests2, nbSchemaRequests, 'There should not be any request because value is cached.');
|
||||
|
||||
passport2.owner(user.id);
|
||||
passport2.owner(function(err, data3) {
|
||||
test.equal(data3.id, user.id, 'Owner should now be the user.');
|
||||
requestsAreCounted && test.equal(nbInitialRequests2 + 1, nbSchemaRequests, 'If we changed owner id, there should be one more request.');
|
||||
// passport2.owner(user.id);
|
||||
// passport2.owner(function(err, data3) {
|
||||
// test.equal(data3.id, user.id, 'Owner should now be the user.');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests2 + 1, nbSchemaRequests, 'If we changed owner id, there should be one more request.');
|
||||
|
||||
passport2.owner(true, function(err, data4) {
|
||||
test.equal(data3.id, data3.id, 'The value should remain the same');
|
||||
requestsAreCounted && test.equal(nbInitialRequests2 + 2, nbSchemaRequests, 'If we forced refreshing, there should be one more request.');
|
||||
test.done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// passport2.owner(true, function(err, data4) {
|
||||
// test.equal(data3.id, data3.id, 'The value should remain the same');
|
||||
// requestsAreCounted && test.equal(nbInitialRequests2 + 2, nbSchemaRequests, 'If we forced refreshing, there should be one more request.');
|
||||
// test.done();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
// });
|
||||
|
||||
if (schema.name !== 'mongoose' && schema.name !== 'neo4j')
|
||||
it('should update or create record', function (test) {
|
||||
|
@ -1157,28 +1147,3 @@ function testOrm(schema) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
function getSpecificTests() {
|
||||
var sp = {};
|
||||
|
||||
sp['neo4j'] = function (schema) {
|
||||
|
||||
it('should create methods for searching by index', function (test) {
|
||||
var Post = schema.models['Post'];
|
||||
test.ok(typeof Post.findByTitle === 'function');
|
||||
Post.create({title: 'Catcher in the rye'}, function (err, post) {
|
||||
if (err) return console.log(err);
|
||||
test.ok(!post.isNewRecord());
|
||||
Post.findByTitle('Catcher in the rye', function (err, foundPost) {
|
||||
if (err) return console.log(err);
|
||||
if (foundPost) {
|
||||
test.equal(post.id, foundPost.id);
|
||||
test.done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return sp;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue