Add Checkpoint model and Model replication methods
This commit is contained in:
parent
4be3395298
commit
1a13a8d95e
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Module Dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Model = require('../loopback').Model
|
||||||
|
, loopback = require('../loopback')
|
||||||
|
, assert = require('assert');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
var properties = {
|
||||||
|
id: {type: Number, generated: true, id: true},
|
||||||
|
time: {type: Number, generated: true, default: Date.now},
|
||||||
|
sourceId: {type: String}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options
|
||||||
|
*/
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkpoint list entry.
|
||||||
|
*
|
||||||
|
* @property id {Number} the sequencial identifier of a checkpoint
|
||||||
|
* @property time {Number} the time when the checkpoint was created
|
||||||
|
* @property sourceId {String} the source identifier
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @inherits {Model}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Checkpoint = module.exports = Model.extend('Checkpoint', properties, options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current checkpoint id
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Number} checkpointId The current checkpoint id
|
||||||
|
*/
|
||||||
|
|
||||||
|
Checkpoint.current = function(cb) {
|
||||||
|
this.find({
|
||||||
|
limit: 1,
|
||||||
|
sort: 'id DESC'
|
||||||
|
}, function(err, checkpoint) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
cb(null, checkpoint.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -131,6 +131,7 @@ function getACL() {
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object
|
||||||
* @param {Boolean} allowed is the request allowed
|
* @param {Boolean} allowed is the request allowed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Model.checkAccess = function(token, modelId, method, callback) {
|
Model.checkAccess = function(token, modelId, method, callback) {
|
||||||
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||||
token = token || ANONYMOUS;
|
token = token || ANONYMOUS;
|
||||||
|
@ -190,3 +191,205 @@ Model._getAccessTypeForMethod = function(method) {
|
||||||
// setup the initial model
|
// setup the initial model
|
||||||
Model.setup();
|
Model.setup();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a set of deltas and conflicts since the given checkpoint.
|
||||||
|
*
|
||||||
|
* See `Change.diff()` for details.
|
||||||
|
*
|
||||||
|
* @param {Number} since Find changes since this checkpoint
|
||||||
|
* @param {Array} remoteChanges An array of change objects
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
Model.diff = function(since, remoteChanges, callback) {
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
Change.diff(this.modelName, since, remoteChanges, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the changes to a model since a given checkpoing. Provide a filter object
|
||||||
|
* to reduce the number of results returned.
|
||||||
|
* @param {Number} since Only return changes since this checkpoint
|
||||||
|
* @param {Object} filter Only include changes that match this filter
|
||||||
|
* (same as `Model.find(filter, ...)`)
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Array} changes An array of `Change` objects
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
Model.changes = function(since, filter, callback) {
|
||||||
|
var idName = this.idName();
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
var model = this;
|
||||||
|
|
||||||
|
filter = filter || {};
|
||||||
|
filter.fields = {};
|
||||||
|
filter.where = filter.where || {};
|
||||||
|
filter.fields[idName] = true;
|
||||||
|
|
||||||
|
// this whole thing could be optimized a bit more
|
||||||
|
Change.find({
|
||||||
|
checkpoint: {gt: since},
|
||||||
|
modelName: this.modelName
|
||||||
|
}, function(err, changes) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
var ids = changes.map(function(change) {
|
||||||
|
return change.modelId;
|
||||||
|
});
|
||||||
|
filter.where[idName] = {inq: ids};
|
||||||
|
model.find(filter, function(err, models) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
var modelIds = models.map(function(m) {
|
||||||
|
return m[idName];
|
||||||
|
});
|
||||||
|
callback(null, changes.filter(function(ch) {
|
||||||
|
return modelIds.indexOf(ch.modelId) > -1;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a checkpoint.
|
||||||
|
*
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
Model.checkpoint = function(cb) {
|
||||||
|
var Checkpoint = this.getChangeModel().Checkpoint;
|
||||||
|
this.getSourceId(function(err, sourceId) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
Checkpoint.create({
|
||||||
|
sourceId: sourceId
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicate changes since the given checkpoint to the given target model.
|
||||||
|
*
|
||||||
|
* @param {Number} since Since this checkpoint
|
||||||
|
* @param {Model} targetModel Target this model class
|
||||||
|
* @options {Object} options
|
||||||
|
* @property {Object} filter Replicate models that match this filter
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Array} conflicts A list of changes that could not be replicated
|
||||||
|
* due to conflicts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Model.replicate = function(since, targetModel, options, callback) {
|
||||||
|
var sourceModel = this;
|
||||||
|
var diff;
|
||||||
|
var updates;
|
||||||
|
|
||||||
|
var tasks = [
|
||||||
|
getLocalChanges,
|
||||||
|
getDiffFromTarget,
|
||||||
|
createSourceUpdates,
|
||||||
|
bulkUpdate,
|
||||||
|
sourceModel.checkpoint.bind(sourceModel)
|
||||||
|
];
|
||||||
|
|
||||||
|
async.waterfall(tasks, function(err) {
|
||||||
|
if(err) return callback(err);
|
||||||
|
callback(null, diff.conflicts);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getLocalChanges(cb) {
|
||||||
|
sourceModel.changes(since, options.filter, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiffFromTarget(sourceChanges, cb) {
|
||||||
|
targetModel.diff(since, sourceChanges, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSourceUpdates(_diff, cb) {
|
||||||
|
diff = _diff;
|
||||||
|
sourceModel.createUpdates(diff.deltas, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bulkUpdate(updates, cb) {
|
||||||
|
targetModel.bulkUpdate(updates, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an update list (for `Model.bulkUpdate()`) from a delta list
|
||||||
|
* (result of `Change.diff()`).
|
||||||
|
*
|
||||||
|
* @param {Array} deltas
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
Model.createUpdates = function(deltas, cb) {
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
var updates = [];
|
||||||
|
var Model = this;
|
||||||
|
var tasks = [];
|
||||||
|
var type = change.type();
|
||||||
|
|
||||||
|
deltas.forEach(function(change) {
|
||||||
|
change = new Change(change);
|
||||||
|
var update = {type: type, change: change};
|
||||||
|
switch(type) {
|
||||||
|
case Change.CREATE:
|
||||||
|
case Change.UPDATE:
|
||||||
|
tasks.push(function(cb) {
|
||||||
|
Model.findById(change.modelId, function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
update.data = inst;
|
||||||
|
updates.push(update);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Change.DELETE:
|
||||||
|
updates.push(update);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async.parallel(tasks, function(err) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
cb(null, updates);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an update list.
|
||||||
|
*
|
||||||
|
* **Note: this is not atomic**
|
||||||
|
*
|
||||||
|
* @param {Array} updates An updates list (usually from Model.createUpdates())
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
Model.bulkUpdate = function(updates, callback) {
|
||||||
|
var tasks = [];
|
||||||
|
var Model = this;
|
||||||
|
var idName = Model.idName();
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
|
||||||
|
updates.forEach(function(update) {
|
||||||
|
switch(update.type) {
|
||||||
|
case Change.UPDATE:
|
||||||
|
case Change.CREATE:
|
||||||
|
tasks.push(Model.upsert.bind(Model, update.data));
|
||||||
|
break;
|
||||||
|
case: Change.DELETE:
|
||||||
|
var data = {};
|
||||||
|
data[idName] = update.change.modelId;
|
||||||
|
var model = new Model(data);
|
||||||
|
tasks.push(model.destroy.bind(model));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async.parallel(tasks, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
Model.getChangeModel = function() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue