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.
*/
var assert = require('assert')
, compat = require('../compat')
, _ = require('underscore');
var assert = require('assert');
var remoting = require('strong-remoting');
var compat = require('../compat');
/**
* Export the RemoteConnector class.
@ -24,6 +24,7 @@ function RemoteConnector(settings) {
this.root = settings.root || '';
this.host = settings.host || 'localhost';
this.port = settings.port || 3000;
this.remotes = remoting.create();
if(settings.url) {
this.url = settings.url;
@ -36,9 +37,9 @@ function RemoteConnector(settings) {
}
RemoteConnector.prototype.connect = function() {
this.remotes.connect(this.url, this.adapter);
}
RemoteConnector.initialize = function(dataSource, callback) {
var connector = dataSource.connector = new RemoteConnector(dataSource.settings);
connector.connect();
@ -48,24 +49,52 @@ RemoteConnector.initialize = function(dataSource, callback) {
RemoteConnector.prototype.define = function(definition) {
var Model = definition.model;
var className = compat.getClassNameForRemoting(Model);
var url = this.url;
var adapter = this.adapter;
var remotes = this.remotes
var SharedClass;
var classes;
var i = 0;
assert(Model.app, 'Cannot attach Model: ' + Model.modelName
+ ' to a RemoteConnector. You must first attach it to an app!');
remotes.exports[className] = Model;
Model.remotes(function(err, remotes) {
var sharedClass = getSharedClass(remotes, className);
remotes.connect(url, adapter);
sharedClass
classes = remotes.classes();
for(; i < classes.length; i++) {
SharedClass = classes[i];
if(SharedClass.name === className) {
SharedClass
.methods()
.forEach(Model.createProxyMethod.bind(Model));
.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) {
return _.find(remotes.classes(), function(sharedClass) {
return sharedClass.name === className;
});
function createProxyMethod(Model, remotes, remoteMethod) {
var scope = remoteMethod.isStatic ? Model : Model.prototype;
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() {}

View File

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

View File

@ -3,10 +3,13 @@
*/
var loopback = require('../loopback');
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 async = require('async');
var assert = require('assert');
var _ = require('underscore');
/**
* The base class for **all models**.
@ -89,6 +92,10 @@ Model.setup = function () {
var ModelCtor = this;
var options = this.settings;
if(options.trackChanges) {
this._defineChangeModel();
}
ModelCtor.sharedCtor = function (data, id, fn) {
if(typeof data === 'function') {
fn = data;
@ -176,12 +183,12 @@ Model.setup = function () {
ModelCtor.sharedCtor.returns = {root: true};
ModelCtor.once('dataSourceAttached', function() {
// enable change tracking (usually for replication)
if(options.trackChanges) {
ModelCtor.once('dataSourceAttached', function() {
ModelCtor.enableChangeTracking();
}
});
}
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
Model.setup();
@ -435,22 +397,39 @@ Model.currentCheckpoint = function(cb) {
/**
* 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
* @options {Object} options
* @property {Object} filter Replicate models that match this filter
* @callback {Function} callback
* @param {Object} [options]
* @param {Object} [options.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
* @param {Conflict[]} conflicts A list of changes that could not be replicated
* due to conflicts.
*/
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 diff;
var updates;
var Change = this.getChangeModel();
var TargetChange = targetModel.getChangeModel();
var changeTrackingEnabled = Change && TargetChange;
assert(
changeTrackingEnabled,
'You must enable change tracking before replicating'
);
var tasks = [
getLocalChanges,
@ -586,18 +565,16 @@ Model.bulkUpdate = function(updates, callback) {
/**
* Get the `Change` model.
*
* @throws {Error} Throws an error if the change model is not correctly setup.
* @return {Change}
*/
Model.getChangeModel = function() {
var changeModel = this.Change;
if(changeModel) return changeModel;
this.Change = changeModel = require('./change').extend(this.modelName + '-change');
var isSetup = changeModel && changeModel.dataSource;
assert(this.dataSource, 'Cannot getChangeModel(): ' + this.modelName
+ ' is not attached to a dataSource');
assert(isSetup, 'Cannot get a setup Change model');
changeModel.attachTo(this.dataSource);
return changeModel;
}
@ -628,9 +605,15 @@ Model.getSourceId = function(cb) {
Model.enableChangeTracking = function() {
var Model = this;
var Change = Model.getChangeModel();
var Change = this.Change || this._defineChangeModel();
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) {
Change.rectifyModelChanges(Model.modelName, [obj.id], function(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-browserify": "~0.2.0",
"karma-mocha": "~0.1.1",
"grunt-karma": "~0.6.2"
"grunt-karma": "~0.6.2",
"loopback-explorer": "~1.1.0"
},
"repository": {
"type": "git",

View File

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

View File

@ -2,61 +2,36 @@ var path = require('path');
var loopback = require('../../');
var models = require('../fixtures/e2e/models');
var TestModel = models.TestModel;
var LocalTestModel = TestModel.extend('LocalTestModel');
var LocalTestModel = TestModel.extend('LocalTestModel', {}, {
trackChanges: true
});
var assert = require('assert');
describe('ReplicationModel', function () {
it('ReplicationModel.enableChangeTracking()', function (done) {
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() {
describe('Replication', function() {
before(function() {
// setup the remote connector
var localApp = loopback();
var ds = loopback.createDataSource({
url: 'http://localhost:3000/api',
connector: loopback.Remote
});
localApp.model(TestModel);
localApp.model(LocalTestModel);
TestModel.attachTo(ds);
var memory = loopback.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) {
var RANDOM = Math.random();
LocalTestModel.create({
foo: 'bar'
}, function() {
n: RANDOM
}, function(err, created) {
LocalTestModel.replicate(0, TestModel, function() {
console.log('replicated');
if(err) return done(err);
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 models = require('./models');
var TestModel = models.TestModel;
var explorer = require('loopback-explorer');
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
var apiPath = '/api';
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.urlNotFound());
app.use(loopback.errorHandler());
app.model(TestModel);
TestModel.attachTo(loopback.memory());

View File

@ -1,4 +1,6 @@
var loopback = require('../../../');
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,
'domain': String,
'email': String
}, {
trackChanges: true
});
});