polyclay-levelup
Version:
levelup persistence adapter for polyclay, the schema-enforcing document mapper
331 lines (279 loc) • 7.35 kB
JavaScript
// Levelup storage interface.
// Objects are stored as hashes.
var
_ = require('lodash'),
assert = require('assert'),
async = require('async'),
fs = require('fs'),
level = require('level'),
path = require('path'),
sublevel = require('level-sublevel'),
indexing = require('level-indexing')
;
var LevelupAdapter = module.exports = function LevelupAdapter()
{
};
LevelupAdapter.prototype.db = null;
LevelupAdapter.prototype.attachdb = null;
LevelupAdapter.prototype.dbname = '';
LevelupAdapter.prototype.objects = null;
LevelupAdapter.prototype.constructor = null;
LevelupAdapter.prototype.configure = function(opts, modelfunc)
{
assert(opts.dbpath || (opts.db && opts.attachdb ), 'you must specify either a db or a dbpath');
assert(typeof modelfunc === 'function', 'you must pass a polyclay model constructor function');
if (opts.db)
{
this.db = opts.db;
this._attachdb = opts.attachdb;
}
else
{
var dir = path.dirname(opts.dbpath);
if (!fs.existsSync(dir))
fs.mkdirSync(dir);
this.db = level(opts.dbpath, {encoding: 'json'});
this._attachdb = level(path.join(opts.dbpath, 'attachments'), {encoding: 'binary'});
}
this.dbname = opts.dbname || modelfunc.prototype.plural;
this.objects = sublevel(this.db).sublevel(this.dbname);
this.attachdb = sublevel(this._attachdb).sublevel(this.dbname);
if (modelfunc.prototype.__index)
{
var self = this;
var indexes = modelfunc.prototype.__index;
if (!Array.isArray(indexes))
indexes = [ indexes ];
indexing(this.objects);
var db = this.objects;
_.each(indexes, function(property)
{
db.index(property);
var getter = 'by' + property[0].toUpperCase() + property.substr(1);
modelfunc[getter] = function(value, callback)
{
db[getter](value, function(err, struct)
{
if (err) return callback(err);
callback(null, self.inflate(struct));
});
};
});
modelfunc.find = function find(input, callback)
{
db.find(input, function(err, struct)
{
if (err) return callback(err);
callback(null, self.inflate(struct));
});
};
}
this.constructor = modelfunc;
};
LevelupAdapter.prototype.provision = function(callback)
{
// Nothing to do?
callback(null);
};
LevelupAdapter.prototype.shutdown = function(callback)
{
var self = this;
self.db.close(function(err)
{
self._attachdb.close(callback);
});
};
LevelupAdapter.prototype.all = function(callback)
{
var keys = [];
this.objects.createKeyStream().on('data', function (data)
{
keys.push(data);
}).on('end', function()
{
callback(null, keys);
}).on('err', function(err)
{
callback(err);
});
};
LevelupAdapter.prototype.keystream = function()
{
return this.objects.createKeyStream();
};
LevelupAdapter.prototype.save = function(object, json, callback)
{
if (!object.key || !object.key.length)
throw(new Error('cannot save a document without a key'));
var self = this;
var attachSub = this.attachdb.sublevel(object.key);
var payload = LevelupAdapter.flatten(json);
var ops = [];
for (var i = 0; i < payload.attachments.length; i++)
{
var k = payload.attachments[i].name;
var body = payload.attachments[i].body;
if (!body || !body.length)
ops.push({ type: 'del', key: k });
else
ops.push({ type: 'put', key: k, value: body });
}
this.objects.put(object.key, payload.body, function(err, response)
{
if (err) return callback(err);
if (ops.length === 0)
return callback(null, 'OK');
attachSub.batch(ops, function(err)
{
callback(err, err ? null : 'OK');
});
});
};
LevelupAdapter.prototype.update = LevelupAdapter.prototype.save;
LevelupAdapter.prototype.get = function(key, callback)
{
var self = this;
if (Array.isArray(key))
return this.getBatch(key, callback);
this.objects.get(key, function(err, payload)
{
if (err && (err.name === 'NotFoundError'))
return callback(null, null);
if (err) return callback(err);
var object = self.inflate(payload);
callback(null, object);
});
};
LevelupAdapter.prototype.stream = function()
{
this.objects.createReadStream();
};
LevelupAdapter.prototype.getBatch = function(keylist, callback)
{
var self = this;
var result = [];
var ptr = 0;
function continuer(err, payload)
{
if (err && (err.name !== 'NotFoundError')) return callback(err);
if (!err)
result.push(self.inflate(payload));
ptr++;
if (ptr >= keylist.length)
return callback(null, result);
self.objects.get(keylist[ptr], continuer);
}
self.objects.get(keylist[ptr], continuer);
};
LevelupAdapter.prototype.merge = function(key, attributes, callback)
{
var self = this;
self.objects.get(key, function(err, payload)
{
if (err) return callback(err);
_.assign(payload, attributes);
self.objects.put(key, payload, callback);
});
};
LevelupAdapter.prototype.remove = function(object, callback)
{
var self = this;
var key;
if (typeof object === 'string')
key = object;
else
key = object.key;
this.objects.del(key, function(err, response)
{
if (err) return callback(err);
self.removeAttachmentsFor(key, callback);
});
};
LevelupAdapter.prototype.removeAttachmentsFor = function(key, callback)
{
var self = this;
var actions = [];
var attachSub = this.attachdb.sublevel(key);
attachSub.createKeyStream().on('data', function (data)
{
actions.push({ type: 'del', key: data });
}).on('end', function()
{
if (actions.length === 0)
return callback(null, 'OK');
attachSub.batch(actions, function(err)
{
callback(err, err ? null : 'OK');
});
}).on('err', function(err)
{
callback(err);
});
};
LevelupAdapter.prototype.destroyMany = function(objects, callback)
{
var self = this;
var actions = [], ops = [], k;
_.each(objects, function(obj)
{
if (typeof obj === 'string')
k = obj;
else
k = obj.key;
ops.push({ type: 'del', key: k });
actions.push(function(cb) { self.removeAttachmentsFor(k, cb); });
});
actions.push(function(cb) { self.objects.batch(ops, cb); });
async.parallel(actions, function(err, replies)
{
if (err) return callback(err);
callback(null, objects.length);
});
};
LevelupAdapter.prototype.attachment = function(key, name, callback)
{
var attachSub = this.attachdb.sublevel(key);
attachSub.get(name, function(err, payload)
{
if (err && err.name === 'NotFoundError')
return callback(null, null);
callback(err, payload);
});
};
LevelupAdapter.prototype.saveAttachment = function(object, attachment, callback)
{
var attachSub = this.attachdb.sublevel(object.key);
attachSub.put(attachment.name, attachment.body, callback);
};
LevelupAdapter.prototype.removeAttachment = function(object, name, callback)
{
this.attachdb.sublevel(object.key).del(name, callback);
};
LevelupAdapter.prototype.inflate = function(payload)
{
if (payload === null)
return;
var object = new this.constructor();
object.initFromStorage(payload);
return object;
};
LevelupAdapter.flatten = function(json)
{
var payload = {};
payload.attachments = [];
if (json._attachments)
{
var attaches = Object.keys(json._attachments);
for (var i = 0; i < attaches.length; i++)
{
var attachment = json._attachments[attaches[i]];
payload.attachments.push({
name: attaches[i],
body: attachment.body
});
}
delete json._attachments;
}
payload.body = _.clone(json);
return payload;
};