Skip to content

Commit

Permalink
[resource, couchdb] .sync() confirmation
Browse files Browse the repository at this point in the history
* Couchdb: update design document only if it needs to be changed
* Resource: .sync(function(err, design, confirm) {...}) if confirm is
function - database engine asks application for migration confirmation
  • Loading branch information
indutny committed Oct 7, 2011
1 parent 0e77ffb commit 0ee8577
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 3 deletions.
216 changes: 216 additions & 0 deletions lib/resourceful/engines/couchdb/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
var sys = require('sys');
var path = require('path');

var resourceful = require('../../../resourceful'),
url = require('url'),
cradle = require('cradle');

var render = require('./view').render;


function Connection(config) {
// Parse uri
if (config.uri) {
var parsed = url.parse('couchdb://' + config.uri);
config.uri = parsed.hostname;
config.port = parseInt(parsed.port, 10);
config.database = (parsed.pathname || '').replace(/^\//, '');
}

this.connection = new(cradle.Connection)({
host: config.uri || '127.0.0.1',
port: config.port || 5984,
raw: true,
cache: false,
auth: config && config.auth || null
}).database(config.database || resourceful.env);
this.cache = new(resourceful.Cache);
};
exports.Connection = Connection;

exports.Connection.prototype = {
protocol: 'couchdb',
load: function (data) {
throw new(Error)("Load not valid for couchdb engine.");
},
request: function (method) {
var args = Array.prototype.slice.call(arguments, 1);
return this.connection[method].apply(this.connection, args);
},
head: function (id, callback) {
return this.request('head', id, callback);
},
get: function (id, callback) {
this.request.call(this, 'get', id, function (e, res) {
if (e) { callback(e) }
else {
if (Array.isArray(id)) {
callback(null, res.rows.map(function (r) { return r.doc }));
} else {
callback(null, res);
}
}
});
},
put: function (id, doc, callback) {
var args = Array.prototype.slice.call(arguments);
return this.request('put', id, doc, function (e, res) {
if (e) {
callback(e);
} else {
res.status = 201;
callback(null, res);
}
});
},
save: function () {
return this.put.apply(this, arguments);
},
update: function (id, doc, callback) {
if (this.cache.has(id)) {
this.put(id, resourceful.mixin({}, this.cache.get(id).toJSON(), doc), callback);
} else {
this.request('merge', id, doc, callback);
}
},
destroy: function () {
var that = this,
args = Array.prototype.slice.call(arguments),
id = args[0];

if (this.cache.has(id)) {
args.splice(1, -1, this.cache.get(id)._rev);
return this.request.apply(this, ['remove'].concat(args));
} else {
this.head(id, function (e, headers) {
if (headers.etag) {
args.splice(1, -1, headers.etag.slice(1, -1));
return that.request.apply(that, ['remove'].concat(args));
} else { args.pop()(e) }
});
}
},
view: function (path, opts, callback) {
return this.request.call(this, 'view', path, opts, function (e, res) {
if (e) { callback(e) }
else {
callback(null, res.rows.map(function (r) {
// With `include_docs=true`, the 'doc' attribute is set instead of 'value'.
var doc = r.doc || r.value;

if (r.id) { doc._id = r.id }
return doc;
}));
}
});
},
all: function (callback) {
return this.request.call(this, 'all', { include_docs: 'true' }, function (e, res) {
if (e) { callback(e) }
else {
callback(null, res.rows.map(function (r) { return r.doc }));
}
});
}
};

Connection.prototype.sync = function (factory, callback) {
var that = this,
id = '_design/' + factory.resource;

callback || (callback = function() {});

factory._design = factory._design || {};
factory._design._id = id;
if (factory._design._rev) { return callback(null) }

// Check if design doc stored in database needs update
function needsUpdate(olddoc, newdoc) {
// If both design docs are w/o view
if (!olddoc.views && !newdoc.views) return false;

// If one of docs has view and other not
if (!olddoc.views && newdoc.views ||
olddoc.views && !newdoc.views) {
return true;
}

// View count should be equal
var oldViews = Object.keys(olddoc.views),
newViews = Object.keys(newdoc.views);

if (oldViews.length !== newViews.length) return true;

var viewChanged = newViews.some(function(name) {
if (!olddoc.views[name]) return true;

if (olddoc.views[name].map !== newdoc.views[name].map) return true;
if (olddoc.views[name].reduce !== newdoc.views[name].reduce) return true;

return false;
});

if (viewChanged) return true;

return false;
};

this.connection.get(id, function (e, doc) {
if (!e && doc) {
factory._design._rev = doc._rev;

if (!needsUpdate(doc, factory._design)) {
// Design document hasn't changed
return callback(null, false);
}
}

// Request confirmation
callback(null, factory._design, function(err, agreed, callback) {
callback || (callback = function() {});

if (err || !agreed) {
return callback(err || 'You should confirm design doc update');
}

that.connection.put(id, factory._design, function (e, res) {
if (e) {
if (e.reason === 'no_db_file') {
that.connection.connection.create(function () {
that.sync(callback);
});
}

/* TODO: Catch errors here. Needs a rewrite, because of the race */
/* condition, when the design doc is trying to be written in parallel */
}
else {
// We might not need to wait for the document to be
// persisted, before returning it. If for whatever reason
// the insert fails, it'll just re-attempt it. For now though,
// to be on the safe side, we wait.
factory._design._rev = res.rev;
callback(null, factory._design);
}
});
});
});
}

//
// Relationship hook
//
exports.Connection._byParent = function (factory, parent, rfactory) {
factory.filter('by' + parent, { include_docs: true }, resourceful.render({
map: function (doc) {
if (doc.resource === $resource) {
for (var i = 0; i < doc.$children.length; i++) {
emit(doc._id, { _id: doc.$children[i] });
}
}
}
}, {
resource: JSON.stringify(parent),
children: factory.resource + '_ids'
}));
}
18 changes: 15 additions & 3 deletions lib/resourceful/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,16 +461,28 @@ Resource.sync = function (callback) {
var that = this,
id = ["_design", this.resource].join('/');

this.once('sync', function (d) { callback(null, d) });
this.once('sync', function (d, confirm) {
callback && callback(null, d, confirm);
});

if (this._synching) {
return;
}

this._synching = true;
this.connection.sync(this, function (e, design) {
this.connection.sync(this, function (e, design, confirm) {
that._synching = false;
that.emit('sync', design);

// Ensure that confirmation callback will be called only once
var once = false;
that.emit('sync', design, confirm && function(err, agreed) {
if (once) return;
once = true;

// Engine may not implement confirmation callback
// Ignore calls to this function if so
confirm(err, agreed);
});
});
};

Expand Down

0 comments on commit 0ee8577

Please sign in to comment.