Fixes for e2e replication / remote connector tests

This commit is contained in:
Ritchie Martori 2014-04-29 17:17:49 -07:00
parent 83e74ab414
commit e35309ba27
9 changed files with 132 additions and 129 deletions

View File

@ -2,9 +2,9 @@
* Dependencies. * Dependencies.
*/ */
var assert = require('assert') var assert = require('assert');
, compat = require('../compat') var remoting = require('strong-remoting');
, _ = require('underscore'); var compat = require('../compat');
/** /**
* Export the RemoteConnector class. * Export the RemoteConnector class.
@ -24,6 +24,7 @@ function RemoteConnector(settings) {
this.root = settings.root || ''; this.root = settings.root || '';
this.host = settings.host || 'localhost'; this.host = settings.host || 'localhost';
this.port = settings.port || 3000; this.port = settings.port || 3000;
this.remotes = remoting.create();
if(settings.url) { if(settings.url) {
this.url = settings.url; this.url = settings.url;
@ -36,9 +37,9 @@ function RemoteConnector(settings) {
} }
RemoteConnector.prototype.connect = function() { RemoteConnector.prototype.connect = function() {
this.remotes.connect(this.url, this.adapter);
} }
RemoteConnector.initialize = function(dataSource, callback) { RemoteConnector.initialize = function(dataSource, callback) {
var connector = dataSource.connector = new RemoteConnector(dataSource.settings); var connector = dataSource.connector = new RemoteConnector(dataSource.settings);
connector.connect(); connector.connect();
@ -48,24 +49,52 @@ RemoteConnector.initialize = function(dataSource, callback) {
RemoteConnector.prototype.define = function(definition) { RemoteConnector.prototype.define = function(definition) {
var Model = definition.model; var Model = definition.model;
var className = compat.getClassNameForRemoting(Model); var className = compat.getClassNameForRemoting(Model);
var url = this.url; var remotes = this.remotes
var adapter = this.adapter; var SharedClass;
var classes;
var i = 0;
assert(Model.app, 'Cannot attach Model: ' + Model.modelName remotes.exports[className] = Model;
+ ' to a RemoteConnector. You must first attach it to an app!');
Model.remotes(function(err, remotes) { classes = remotes.classes();
var sharedClass = getSharedClass(remotes, className);
remotes.connect(url, adapter); for(; i < classes.length; i++) {
sharedClass SharedClass = classes[i];
.methods() if(SharedClass.name === className) {
.forEach(Model.createProxyMethod.bind(Model)); SharedClass
}); .methods()
.forEach(function(remoteMethod) {
// TODO(ritch) more elegant way of ignoring a nested shared class
if(remoteMethod.name !== 'Change'
&& remoteMethod.name !== 'Checkpoint') {
createProxyMethod(Model, remotes, remoteMethod);
}
});
return;
}
}
} }
function getSharedClass(remotes, className) { function createProxyMethod(Model, remotes, remoteMethod) {
return _.find(remotes.classes(), function(sharedClass) { var scope = remoteMethod.isStatic ? Model : Model.prototype;
return sharedClass.name === className; var original = scope[remoteMethod.name];
});
var fn = scope[remoteMethod.name] = function remoteMethodProxy() {
var args = Array.prototype.slice.call(arguments);
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
var callback;
if(lastArgIsFunc) {
callback = args.pop();
}
remotes.invoke(remoteMethod.stringName, args, callback);
}
for(var key in original) {
fn[key] = original[key];
}
fn._delegate = true;
} }
function noop() {} function noop() {}

View File

@ -2,7 +2,7 @@
* Module Dependencies. * Module Dependencies.
*/ */
var Model = require('../loopback').Model var DataModel = require('./data-model')
, loopback = require('../loopback') , loopback = require('../loopback')
, crypto = require('crypto') , crypto = require('crypto')
, CJSON = {stringify: require('canonical-json')} , CJSON = {stringify: require('canonical-json')}
@ -44,7 +44,7 @@ var options = {
* @inherits {Model} * @inherits {Model}
*/ */
var Change = module.exports = Model.extend('Change', properties, options); var Change = module.exports = DataModel.extend('Change', properties, options);
/*! /*!
* Constants * Constants
@ -271,6 +271,7 @@ Change.prototype.getModelCtor = function() {
*/ */
Change.prototype.equals = function(change) { Change.prototype.equals = function(change) {
if(!change) return false;
return change.rev === this.rev; return change.rev === this.rev;
} }
@ -337,9 +338,10 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
var localModelIds = []; var localModelIds = [];
localChanges.forEach(function(localChange) { localChanges.forEach(function(localChange) {
localChange = new Change(localChange);
localModelIds.push(localChange.modelId); localModelIds.push(localChange.modelId);
var remoteChange = remoteChangeIndex[localChange.modelId]; var remoteChange = remoteChangeIndex[localChange.modelId];
if(!localChange.equals(remoteChange)) { if(remoteChange && !localChange.equals(remoteChange)) {
if(remoteChange.isBasedOn(localChange)) { if(remoteChange.isBasedOn(localChange)) {
deltas.push(remoteChange); deltas.push(remoteChange);
} else { } else {

View File

@ -3,10 +3,13 @@
*/ */
var loopback = require('../loopback'); var loopback = require('../loopback');
var compat = require('../compat'); var compat = require('../compat');
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder; var juggler = require('loopback-datasource-juggler');
var ModelBuilder = juggler.ModelBuilder;
var DataSource = juggler.DataSource;
var modeler = new ModelBuilder(); var modeler = new ModelBuilder();
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
var _ = require('underscore');
/** /**
* The base class for **all models**. * The base class for **all models**.
@ -89,6 +92,10 @@ Model.setup = function () {
var ModelCtor = this; var ModelCtor = this;
var options = this.settings; var options = this.settings;
if(options.trackChanges) {
this._defineChangeModel();
}
ModelCtor.sharedCtor = function (data, id, fn) { ModelCtor.sharedCtor = function (data, id, fn) {
if(typeof data === 'function') { if(typeof data === 'function') {
fn = data; fn = data;
@ -176,12 +183,12 @@ Model.setup = function () {
ModelCtor.sharedCtor.returns = {root: true}; ModelCtor.sharedCtor.returns = {root: true};
ModelCtor.once('dataSourceAttached', function() { // enable change tracking (usually for replication)
// enable change tracking (usually for replication) if(options.trackChanges) {
if(options.trackChanges) { ModelCtor.once('dataSourceAttached', function() {
ModelCtor.enableChangeTracking(); ModelCtor.enableChangeTracking();
} });
}); }
return ModelCtor; return ModelCtor;
}; };
@ -294,51 +301,6 @@ Model.getApp = function(callback) {
} }
} }
/**
* Get the Model's `RemoteObjects`.
*
* @callback {Function} callback
* @param {Error} err
* @param {RemoteObjects} remoteObjects
* @end
*/
Model.remotes = function(callback) {
this.getApp(function(err, app) {
callback(null, app.remotes());
});
}
/*!
* Create a proxy function for invoking remote methods.
*
* @param {SharedMethod} sharedMethod
*/
Model.createProxyMethod = function createProxyFunction(remoteMethod) {
var Model = this;
var scope = remoteMethod.isStatic ? Model : Model.prototype;
var original = scope[remoteMethod.name];
var fn = scope[remoteMethod.name] = function proxy() {
var args = Array.prototype.slice.call(arguments);
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
var callback;
if(lastArgIsFunc) {
callback = args.pop();
}
Model.remotes(function(err, remotes) {
remotes.invoke(remoteMethod.stringName, args, callback);
});
}
for(var key in original) {
fn[key] = original[key];
}
fn._delegate = true;
}
// setup the initial model // setup the initial model
Model.setup(); Model.setup();
@ -435,22 +397,39 @@ Model.currentCheckpoint = function(cb) {
/** /**
* Replicate changes since the given checkpoint to the given target model. * Replicate changes since the given checkpoint to the given target model.
* *
* @param {Number} since Since this checkpoint * @param {Number} [since] Since this checkpoint
* @param {Model} targetModel Target this model class * @param {Model} targetModel Target this model class
* @options {Object} options * @param {Object} [options]
* @property {Object} filter Replicate models that match this filter * @param {Object} [options.filter] Replicate models that match this filter
* @callback {Function} callback * @callback {Function} [callback]
* @param {Error} err * @param {Error} err
* @param {Array} conflicts A list of changes that could not be replicated * @param {Conflict[]} conflicts A list of changes that could not be replicated
* due to conflicts. * due to conflicts.
*/ */
Model.replicate = function(since, targetModel, options, callback) { Model.replicate = function(since, targetModel, options, callback) {
var lastArg = arguments[arguments.length - 1];
if(typeof lastArg === 'function' && arguments.length > 1) {
callback = lastArg;
}
if(typeof since === 'funciton' && since.modelName) {
since = -1;
targetModel = since;
}
var sourceModel = this; var sourceModel = this;
var diff; var diff;
var updates; var updates;
var Change = this.getChangeModel(); var Change = this.getChangeModel();
var TargetChange = targetModel.getChangeModel(); var TargetChange = targetModel.getChangeModel();
var changeTrackingEnabled = Change && TargetChange;
assert(
changeTrackingEnabled,
'You must enable change tracking before replicating'
);
var tasks = [ var tasks = [
getLocalChanges, getLocalChanges,
@ -586,18 +565,16 @@ Model.bulkUpdate = function(updates, callback) {
/** /**
* Get the `Change` model. * Get the `Change` model.
* *
* @throws {Error} Throws an error if the change model is not correctly setup.
* @return {Change} * @return {Change}
*/ */
Model.getChangeModel = function() { Model.getChangeModel = function() {
var changeModel = this.Change; var changeModel = this.Change;
if(changeModel) return changeModel; var isSetup = changeModel && changeModel.dataSource;
this.Change = changeModel = require('./change').extend(this.modelName + '-change');
assert(this.dataSource, 'Cannot getChangeModel(): ' + this.modelName assert(isSetup, 'Cannot get a setup Change model');
+ ' is not attached to a dataSource');
changeModel.attachTo(this.dataSource);
return changeModel; return changeModel;
} }
@ -628,9 +605,15 @@ Model.getSourceId = function(cb) {
Model.enableChangeTracking = function() { Model.enableChangeTracking = function() {
var Model = this; var Model = this;
var Change = Model.getChangeModel(); var Change = this.Change || this._defineChangeModel();
var cleanupInterval = Model.settings.changeCleanupInterval || 30000; var cleanupInterval = Model.settings.changeCleanupInterval || 30000;
assert(this.dataSource, 'Cannot enableChangeTracking(): ' + this.modelName
+ ' is not attached to a dataSource');
Change.attachTo(this.dataSource);
Change.getCheckpointModel().attachTo(this.dataSource);
Model.on('changed', function(obj) { Model.on('changed', function(obj) {
Change.rectifyModelChanges(Model.modelName, [obj.id], function(err) { Change.rectifyModelChanges(Model.modelName, [obj.id], function(err) {
if(err) { if(err) {
@ -666,3 +649,8 @@ Model.enableChangeTracking = function() {
}); });
} }
} }
Model._defineChangeModel = function() {
var BaseChangeModel = require('./change');
return this.Change = BaseChangeModel.extend(this.modelName + '-change');
}

View File

@ -53,7 +53,8 @@
"karma": "~0.10.9", "karma": "~0.10.9",
"karma-browserify": "~0.2.0", "karma-browserify": "~0.2.0",
"karma-mocha": "~0.1.1", "karma-mocha": "~0.1.1",
"grunt-karma": "~0.6.2" "grunt-karma": "~0.6.2",
"loopback-explorer": "~1.1.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -7,12 +7,10 @@ var assert = require('assert');
describe('RemoteConnector', function() { describe('RemoteConnector', function() {
before(function() { before(function() {
// setup the remote connector // setup the remote connector
var localApp = loopback();
var ds = loopback.createDataSource({ var ds = loopback.createDataSource({
url: 'http://localhost:3000/api', url: 'http://localhost:3000/api',
connector: loopback.Remote connector: loopback.Remote
}); });
localApp.model(TestModel);
TestModel.attachTo(ds); TestModel.attachTo(ds);
}); });

View File

@ -2,61 +2,36 @@ var path = require('path');
var loopback = require('../../'); var loopback = require('../../');
var models = require('../fixtures/e2e/models'); var models = require('../fixtures/e2e/models');
var TestModel = models.TestModel; var TestModel = models.TestModel;
var LocalTestModel = TestModel.extend('LocalTestModel'); var LocalTestModel = TestModel.extend('LocalTestModel', {}, {
trackChanges: true
});
var assert = require('assert'); var assert = require('assert');
describe('ReplicationModel', function () { describe('Replication', function() {
it('ReplicationModel.enableChangeTracking()', function (done) { before(function() {
var TestReplicationModel = loopback.DataModel.extend('TestReplicationModel');
var remote = loopback.createDataSource({
url: 'http://localhost:3000/api',
connector: loopback.Remote
});
var testApp = loopback();
testApp.model(TestReplicationModel);
TestReplicationModel.attachTo(remote);
// chicken-egg condition
// getChangeModel() requires it to be attached to an app
// attaching to the app requires getChangeModel()
var Change = TestReplicationModel.getChangeModel();
testApp.model(Change);
Change.attachTo(remote);
TestReplicationModel.enableChangeTracking();
});
});
describe.skip('Replication', function() {
beforeEach(function() {
// setup the remote connector // setup the remote connector
var localApp = loopback();
var ds = loopback.createDataSource({ var ds = loopback.createDataSource({
url: 'http://localhost:3000/api', url: 'http://localhost:3000/api',
connector: loopback.Remote connector: loopback.Remote
}); });
localApp.model(TestModel);
localApp.model(LocalTestModel);
TestModel.attachTo(ds); TestModel.attachTo(ds);
var memory = loopback.memory(); var memory = loopback.memory();
LocalTestModel.attachTo(memory); LocalTestModel.attachTo(memory);
// TODO(ritch) this should be internal...
LocalTestModel.getChangeModel().attachTo(memory);
LocalTestModel.enableChangeTracking();
// failing because change model is not properly attached
TestModel.enableChangeTracking();
}); });
it('should replicate local data to the remote', function (done) { it('should replicate local data to the remote', function (done) {
var RANDOM = Math.random();
LocalTestModel.create({ LocalTestModel.create({
foo: 'bar' n: RANDOM
}, function() { }, function(err, created) {
LocalTestModel.replicate(0, TestModel, function() { LocalTestModel.replicate(0, TestModel, function() {
console.log('replicated'); if(err) return done(err);
done(); TestModel.findOne({n: RANDOM}, function(err, found) {
assert.equal(created.id, found.id);
done();
});
}); });
}); });
}); });
}); });

View File

@ -3,12 +3,18 @@ var path = require('path');
var app = module.exports = loopback(); var app = module.exports = loopback();
var models = require('./models'); var models = require('./models');
var TestModel = models.TestModel; var TestModel = models.TestModel;
var explorer = require('loopback-explorer');
app.use(loopback.cookieParser({secret: app.get('cookieSecret')})); app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
var apiPath = '/api'; var apiPath = '/api';
app.use(apiPath, loopback.rest()); app.use(apiPath, loopback.rest());
TestModel.attachTo(loopback.memory());
app.model(TestModel);
app.model(TestModel.getChangeModel());
app.use('/explorer', explorer(app, {basePath: apiPath}));
app.use(loopback.static(path.join(__dirname, 'public'))); app.use(loopback.static(path.join(__dirname, 'public')));
app.use(loopback.urlNotFound()); app.use(loopback.urlNotFound());
app.use(loopback.errorHandler()); app.use(loopback.errorHandler());
app.model(TestModel);
TestModel.attachTo(loopback.memory());

View File

@ -1,4 +1,6 @@
var loopback = require('../../../'); var loopback = require('../../../');
var DataModel = loopback.DataModel; var DataModel = loopback.DataModel;
exports.TestModel = DataModel.extend('TestModel'); exports.TestModel = DataModel.extend('TestModel', {}, {
trackChanges: true
});

View File

@ -18,6 +18,8 @@ describe('Model', function() {
'gender': String, 'gender': String,
'domain': String, 'domain': String,
'email': String 'email': String
}, {
trackChanges: true
}); });
}); });