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