Fixes for e2e replication / remote connector tests
This commit is contained in:
parent
83e74ab414
commit
e35309ba27
|
@ -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() {}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
var loopback = require('../../../');
|
||||
var DataModel = loopback.DataModel;
|
||||
|
||||
exports.TestModel = DataModel.extend('TestModel');
|
||||
exports.TestModel = DataModel.extend('TestModel', {}, {
|
||||
trackChanges: true
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ describe('Model', function() {
|
|||
'gender': String,
|
||||
'domain': String,
|
||||
'email': String
|
||||
}, {
|
||||
trackChanges: true
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue