sails-cloudant-orm
Version:
Waterline Sails Adapter for IBM Cloudant Database
613 lines (491 loc) • 18.4 kB
JavaScript
var nano = require('nano');
var async = require('async');
var extend = require('xtend');
var cookie = require('cookie');
var DeepMerge = require('deep-merge');
var _ = require('underscore');
var merge = DeepMerge(function(a, b) {
return b;
});
var registry = require('./registry');
var views = require('./views');
/**
* Sails Boilerplate Adapter
*
* Most of the methods below are optional.
*
* If you don't need / can't get to every method, just implement
* what you have time for. The other methods will only fail if
* you try to call them!
*
* For many adapters, this file is all you need. For very complex adapters, you may need more flexiblity.
* In any case, it's probably a good idea to start with one file and refactor only if necessary.
* If you do go that route, it's conventional in Node to create a `./lib` directory for your private submodules
* and load them at the top of the file with other dependencies. e.g. var update = `require('./lib/update')`;
*/
// You'll want to maintain a reference to each collection
// (aka model) that gets registered with this adapter.
// You may also want to store additional, private data
// per-collection (esp. if your data store uses persistent
// connections).
//
// Keep in mind that models can be configured to use different databases
// within the same app, at the same time.
//
// i.e. if you're writing a MariaDB adapter, you should be aware that one
// model might be configured as `host="localhost"` and another might be using
// `host="foo.com"` at the same time. Same thing goes for user, database,
// password, or any other config.
//
// You don't have to support this feature right off the bat in your
// adapter, but it ought to get done eventually.
//
// Sounds annoying to deal with...
// ...but it's not bad. In each method, acquire a connection using the config
// for the current model (looking it up from `_modelReferences`), establish
// a connection, then tear it down before calling your method's callback.
// Finally, as an optimization, you might use a db pool for each distinct
// connection configuration, partioning pools for each separate configuration
// for your adapter (i.e. worst case scenario is a pool for each model, best case
// scenario is one single single pool.) For many databases, any change to
// host OR database OR user OR password = separate pool.
var adapter = exports;
// Set to true if this adapter supports (or requires) things like data types, validations, keys, etc.
// If true, the schema for models using this adapter will be automatically synced when the server starts.
// Not terribly relevant if your data store is not SQL/schemaful.
adapter.syncable = false,
// Reserved attributes.
// These attributes get passed in to the `adapter.update` function even if they're not declared
// in the model schema.
adapter.reservedAttributes = ['id', 'rev'];
// Default configuration for collections
// (same effect as if these properties were included at the top level of the model definitions)
adapter.defaults = {
port: 443,
host: 'localhost',
https: false,
username: null,
password: null,
schema: true,
syncable: false,
autoPK: false,
pkFormat: 'string',
maxMergeAttempts: 5,
// If setting syncable, you should consider the migrate option,
// which allows you to set how the sync will be performed.
// It can be overridden globally in an app (config/adapters.js)
// and on a per-model basis.
//
// IMPORTANT:
// `migrate` is not a production data migration solution!
// In production, always use `migrate: safe`
//
// drop => Drop schema and data, then recreate it
// alter => Drop/add columns as necessary.
// safe => Don't change anything (good for production DBs)
migrate: 'safe'
};
/**
*
* This method runs when a model is initially registered
* at server-start-time. This is the only required method.
*
* @param {[type]} collection [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.registerConnection = function registerConnection(connection, collections, cb) {
var url = urlForConfig(connection);
var db = nano(url);
// Save the connection
registry.connection(connection.identity, connection);
//console.log("Start registering connection");
//console.log(Object.keys(collections));
async.each(Object.keys(collections), function(model, cb) {
//console.log("Register "+model);
adapter.registerSingleCollection(connection, model, collections[model], cb);
}, function(err) {
//console.log("Done registering connection");
//console.log(err);
if (err) {
//console.log("Problem!");
cb(new Error("Problem when registering Collections"));
} else {
//console.log("Success registering connections!");
cb();
}
});
};
/**
*
* This method runs to register a single model, or collection.
*
* @param {[type]} connection [description]
* @param {[type]} collection [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.registerSingleCollection = function registerCollection(connection, collectionName, collection, cb) {
var url = urlForConfig(connection);
var db = nano(url);
db.get(collectionName, gotDatabase);
function gotDatabase(err) {
if (err && err.status_code == 404 && err.reason == 'no_db_file') {
db.create(collectionName, createdDB);
} else {
registry.collection(collectionName, collection);
registry.db(collectionName, nano(url + collectionName));
cb();
}
}
function createdDB(err) {
if (err) {
cb(err);
} else {
adapter.registerSingleCollection(connection, collectionName, collection, cb);
}
}
};
/**
* Fired when a model is unregistered, typically when the server
* is killed. Useful for tearing-down remaining open connections,
* etc.
*
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.teardown = function teardown(connection, cb) {
process.nextTick(cb);
};
/**
*
* REQUIRED method if integrating with a schemaful
* (SQL-ish) database.
*
* @param {[type]} collectionName [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.describe = function describe(connection, collectionName, cb) {
var collection = registry.collection(collectionName);
if (!collection)
return cb(new Error('no such collection'));
return cb(null, collection.definition);
};
/**
*
*
* REQUIRED method if integrating with a schemaful
* (SQL-ish) database.
*
* @param {[type]} collectionName [description]
* @param {[type]} relations [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.drop = function drop(connectionName, collectionName, relations, cb) {
var url = urlForConfig(connectionName);
var db = nano(url);
db.destroy(collectionName, cb);
};
/**
*
* REQUIRED method if users expect to call Model.find(), Model.findOne(),
* or related.
*
* You should implement this method to respond with an array of instances.
* Waterline core will take care of supporting all the other different
* find methods/usages.
*
* @param {[type]} collectionName [description]
* @param {[type]} options [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.find = find;
function find(connectionName, collectionName, options, cb, round) {
if ('number' != typeof round) round = 0;
// If you need to access your private data for this collection:
var db = registry.db(collectionName);
var dbOptions = {};
if (options.limit) dbOptions.limit = options.limit;
if (options.skip) dbOptions.skip = options.skip;
var queriedAttributes = Object.keys(options.where || {});
//console.log("Queried Attributes: ",queriedAttributes);
if (queriedAttributes.length == 0) {
//console.log("Queried Attributes doesn't contain any values");
/// All docs
dbOptions.include_docs = true;
db.list(dbOptions, listReplied);
} else if (queriedAttributes.length == 1 && (queriedAttributes[0] == 'id' || queriedAttributes[0] == '_id')) {
var id = options.where.id || options.where._id;
/// One doc by id
db.get(id, dbOptions, function(err, doc) {
if (err && err.status_code == 404) cb(null, []);
else if (err) cb(err);
else {
var docs;
if (doc) docs = [doc];
else docs = [];
cb(null, docs.map(docForReply));
}
});
} else if (options.where.like) {
//console.log("Query by where: ",options.where.like);
var viewName = views.name(options.where.like);
var value = views.likeValue(options.where.like, true);
dbOptions.startkey = value.startkey;
dbOptions.endkey = value.endkey;
db.view('views', viewName, dbOptions, viewResult);
} else {
//console.log("Lets look with a view: ",options.where);
var viewName = views.name(options.where);
dbOptions.key = views.value(options.where);
db.view('views', viewName, dbOptions, viewResult);
}
function listReplied(err, docs) {
if (err) cb(err);
else {
if (!Array.isArray(docs) && docs.rows) docs = docs.rows.map(prop('doc'));
cb(null, docs.map(docForReply))
};
}
function viewResult(err, reply) {
if (err && err.status_code == 404 && round < 1)
views.create(db, options.where.like || options.where, createdView);
else if (err) cb(err);
else cb(null, reply.rows.map(prop('value')).map(docForReply));
}
function createdView(err) {
if (err) cb(err);
else find.call(connectionName, connectionName, collectionName, options, cb, round + 1);
}
};
/**
*
* REQUIRED method if users expect to call Model.create() or any methods
*
* @param {[type]} collectionName [description]
* @param {[type]} values [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.create = function create(connectionName, collectionName, values, cb) {
var db = registry.db(collectionName);
db.insert(docForIngestion(values), replied);
function replied(err, reply) {
if (err) cb(err);
else {
var attrs = extend({}, values, { _id: reply.id, _rev: reply.rev });
cb(null, docForReply(attrs));
}
}
};
/**
*
*
* REQUIRED method if users expect to call Model.update()
*
* @param {[type]} collectionName [description]
* @param {[type]} options [description]
* @param {[type]} values [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.update = function update(connectionName, collectionName, options, values, cb) {
adapter.merge(connectionName, collectionName, values.id, values, replied, 1);
/*
adapter.merge = function adapterMerge(connectionName, collectionName, id, attrs, cb, attempts) {
var searchAttributes = Object.keys(options.where);
if (searchAttributes.length != 1)
return cb(new Error('only support updating one object by id'));
if (searchAttributes[0] != 'id')
return cb(new Error('only support updating one object by id'));
var db = registry.db(collectionName);
console.log("Saving "+collectionName+" with id ",options,values);
db.insert(docForIngestion(values), options.where.id, replied);
*/
function replied(err, reply) {
if (err) cb(err);
else {
var attrs = extend({}, values, { _id: reply.id, _rev: reply.rev });
cb(null, docForReply(attrs));
}
}
};
/**
*
* REQUIRED method if users expect to call Model.destroy()
*
* @param {[type]} collectionName [description]
* @param {[type]} options [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
adapter.destroy = function destroy(connectionName, collectionName, options, cb) {
// Find the record
var db = registry.db(collectionName);
adapter.find(connectionName, collectionName, options, function(err, docs) {
async.each(docs, function(item, localCb) {
db.destroy(item.id, item.rev, localCb);
}, cb);
});
};
/**********************************************
* Custom methods
**********************************************/
/// Authenticate
adapter.authenticate = function authenticate(connection, collectionName, username, password, cb) {
var db = registry.db(collectionName);
db.auth(username, password, replied);
function replied(err, body, headers) {
if (err) cb(err);
else {
var sessionId;
var header = headers['set-cookie'][0];
if (header) sessionId = cookie.parse(header).AuthSession;
cb(null, sessionId, username, body.roles);
}
}
};
/// Session
adapter.session = function session(connection, collectionName, sid, cb) {
var url = urlForConfig(registry.connection(connection));
var sessionDb = nano({
url: url,
cookie: 'AuthSession=' + encodeURIComponent(sid)
});
sessionDb.session(cb);
};
/// Merge
adapter.merge = function adapterMerge(connectionName, collectionName, id, attrs, cb, attempts) {
var doc;
var db = registry.db(collectionName);
var coll = registry.collection(collectionName);
/*
console.log('------------------------------------------');
console.log('Attempting merge on ',collectionName,id,attrs);
console.log('------------------------------------------');
*/
if ('number' != typeof attempts) attempts = 0;
else if (attempts > 0) {
var config = coll.adapter.config;
// Reference to maxMergeAttempts
if (attempts > 5) {
return cb(new Error('max attempts of merging reached'));
}
}
db.get(id, got);
function got(err, _doc) {
if (err && err.status_code == 404) _doc = {};
else if (err) return cb(err);
delete attrs._rev;
_doc = docForReply(_doc);
doc = merge(_doc, attrs);
//console.log('----------Callbacks',coll._callbacks.beforeUpdate);
async.eachSeries(coll._callbacks.beforeUpdate || [], invokeCallback, afterBeforeUpdate);
}
function invokeCallback(fn, cb) {
//console.log("----------Calling Function ",fn);
fn.call(null, doc, cb);
}
function afterBeforeUpdate(err) {
if (err) return cb(err);
var newdoc = docForIngestion(doc);
//console.log('----------Heres our final doc',newdoc._id,newdoc._rev);
//console.trace();
db.insert(newdoc, id, saved);
}
function saved(err, reply) {
if (err && err.status_code == 409) {
//console.log('Calling merge again!');
adapter.merge(connectionName, collectionName, id, attrs, cb, attempts + 1)
} else if (err) cb(err);
else {
extend(doc, { _rev: reply.rev, _id: reply.id });
doc = docForReply(doc);
callbackAfter();
}
}
function callbackAfter() {
async.eachSeries(coll._callbacks.afterUpdate || [], invokeCallback, finalCallback);
}
function finalCallback(err) {
if (err) cb(err);
else cb(null, doc);
}
};
/// View
adapter.view = function view(connectionName, collectionName, viewName, options, cb, round) {
if ('number' != typeof round) round = 0;
var db = registry.db(collectionName);
db.view('views', viewName, options, viewResult);
function viewResult(err, results) {
if (err && err.status_code == 404 && round < 2)
populateView(connectionName, collectionName, viewName, populatedView);
else if (err) cb(err);
else cb(null, (results && results.rows && results.rows || []).map(prop('value')).map(docForReply));
}
function populatedView(err) {
if (err) cb(err);
else adapter.view(connectionName, collectionName, viewName, options, cb, round + 1);
}
};
function populateView(connectionName, collectionName, viewName, cb) {
var collection = registry.collection(collectionName);
var view = collection.views && collection.views[viewName];
if (!view) return cb(new Error('No view named ' + viewName + ' defined in model ' + collectionName));
else {
var db = registry.db(collectionName);
db.get('_design/views', gotDDoc);
}
function gotDDoc(err, ddoc) {
if (!ddoc) ddoc = {};
if (!ddoc.views) ddoc.views = {};
if (!ddoc._id) ddoc._id = '_design/views';
ddoc.views[viewName] = view;
ddoc.language = 'javascript';
db.insert(ddoc, insertedDDoc);
}
function insertedDDoc(err) {
cb(err);
}
}
/// Utils
function urlForConfig(config) {
var schema = 'http';
if (config.https) schema += 's';
var auth = '';
if (config.username && config.password) {
auth = encodeURIComponent(config.username) + ':' + encodeURIComponent(config.password) + '@';
}
return [schema, '://', auth, config.host, ':', config.port, '/', config.database, '/'].join('');
}
function prop(p) {
return function(o) {
return o[p];
};
}
function docForReply(doc) {
if (doc._id) {
doc.id = doc._id;
delete doc._id;
}
if (doc._rev) {
doc.rev = doc._rev;
delete doc._rev;
}
return doc;
}
function docForIngestion(doc) {
doc = extend({}, doc);
if (doc.id) {
doc._id = doc.id;
delete doc.id;
}
if (doc.rev) {
doc._rev = doc.rev;
delete doc.rev;
}
return doc;
}