Merge pull request #1445 from strongloop/feature/subscribe
Add PersistedModel.subscribe()
This commit is contained in:
commit
cdddb08a67
|
@ -7,6 +7,7 @@ var assert = require('assert');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var deprecated = require('depd')('loopback');
|
var deprecated = require('depd')('loopback');
|
||||||
var debug = require('debug')('loopback:persisted-model');
|
var debug = require('debug')('loopback:persisted-model');
|
||||||
|
var PassThrough = require('stream').PassThrough;
|
||||||
|
|
||||||
module.exports = function(registry) {
|
module.exports = function(registry) {
|
||||||
var Model = registry.getModel('Model');
|
var Model = registry.getModel('Model');
|
||||||
|
@ -761,6 +762,24 @@ module.exports = function(registry) {
|
||||||
http: {verb: 'post', path: '/:id/rectify-change'}
|
http: {verb: 'post', path: '/:id/rectify-change'}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'createChangeStream', {
|
||||||
|
description: 'Create a change stream.',
|
||||||
|
accessType: 'READ',
|
||||||
|
http: [
|
||||||
|
{verb: 'post', path: '/change-stream'},
|
||||||
|
{verb: 'get', path: '/change-stream'}
|
||||||
|
],
|
||||||
|
accepts: {
|
||||||
|
arg: 'options',
|
||||||
|
type: 'object'
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
arg: 'changes',
|
||||||
|
type: 'ReadableStream',
|
||||||
|
json: true
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1527,6 +1546,101 @@ module.exports = function(registry) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a change stream.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Object} options.where Only changes to models matching this where filter will be included in the `ChangeStream`.
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {ChangeStream} changes
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.createChangeStream = function(options, cb) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
cb = options;
|
||||||
|
options = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idName = this.getIdName();
|
||||||
|
var Model = this;
|
||||||
|
var changes = new PassThrough({objectMode: true});
|
||||||
|
var writeable = true;
|
||||||
|
|
||||||
|
changes.destroy = function() {
|
||||||
|
changes.removeAllListeners('error');
|
||||||
|
changes.removeAllListeners('end');
|
||||||
|
writeable = false;
|
||||||
|
changes = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
changes.on('error', function() {
|
||||||
|
writeable = false;
|
||||||
|
});
|
||||||
|
changes.on('end', function() {
|
||||||
|
writeable = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
process.nextTick(function() {
|
||||||
|
cb(null, changes);
|
||||||
|
});
|
||||||
|
|
||||||
|
Model.observe('after save', createChangeHandler('save'));
|
||||||
|
Model.observe('after delete', createChangeHandler('delete'));
|
||||||
|
|
||||||
|
function createChangeHandler(type) {
|
||||||
|
return function(ctx, next) {
|
||||||
|
// since it might have set to null via destroy
|
||||||
|
if (!changes) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
var where = ctx.where;
|
||||||
|
var data = ctx.instance || ctx.data;
|
||||||
|
var whereId = where && where[idName];
|
||||||
|
|
||||||
|
// the data includes the id
|
||||||
|
// or the where includes the id
|
||||||
|
var target;
|
||||||
|
|
||||||
|
if (data && (data[idName] || data[idName] === 0)) {
|
||||||
|
target = data[idName];
|
||||||
|
} else if (where && (where[idName] || where[idName] === 0)) {
|
||||||
|
target = where[idName];
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasTarget = target === 0 || !!target;
|
||||||
|
|
||||||
|
var change = {
|
||||||
|
target: target,
|
||||||
|
where: where,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'save':
|
||||||
|
if (ctx.isNewInstance === undefined) {
|
||||||
|
change.type = hasTarget ? 'update' : 'create';
|
||||||
|
} else {
|
||||||
|
change.type = ctx.isNewInstance ? 'create' : 'update';
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
change.type = 'remove';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(ritch) this is ugly... maybe a ReadableStream would be better
|
||||||
|
if (writeable) {
|
||||||
|
changes.write(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
PersistedModel.setup();
|
PersistedModel.setup();
|
||||||
|
|
||||||
return PersistedModel;
|
return PersistedModel;
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
describe('PersistedModel.createChangeStream()', function() {
|
||||||
|
describe('configured to source changes locally', function() {
|
||||||
|
before(function() {
|
||||||
|
var test = this;
|
||||||
|
var app = loopback({localRegistry: true});
|
||||||
|
var ds = app.dataSource('ds', {connector: 'memory'});
|
||||||
|
this.Score = app.model('Score', {
|
||||||
|
dataSource: 'ds',
|
||||||
|
changeDataSource: false // use only local observers
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect create', function(done) {
|
||||||
|
var Score = this.Score;
|
||||||
|
|
||||||
|
Score.createChangeStream(function(err, changes) {
|
||||||
|
changes.on('data', function(change) {
|
||||||
|
expect(change.type).to.equal('create');
|
||||||
|
changes.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
Score.create({team: 'foo'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect update', function(done) {
|
||||||
|
var Score = this.Score;
|
||||||
|
Score.create({team: 'foo'}, function(err, newScore) {
|
||||||
|
Score.createChangeStream(function(err, changes) {
|
||||||
|
changes.on('data', function(change) {
|
||||||
|
expect(change.type).to.equal('update');
|
||||||
|
changes.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
newScore.updateAttributes({
|
||||||
|
bat: 'baz'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect delete', function(done) {
|
||||||
|
var Score = this.Score;
|
||||||
|
Score.create({team: 'foo'}, function(err, newScore) {
|
||||||
|
Score.createChangeStream(function(err, changes) {
|
||||||
|
changes.on('data', function(change) {
|
||||||
|
expect(change.type).to.equal('remove');
|
||||||
|
changes.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
newScore.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(ritch) implement multi-server support
|
||||||
|
describe.skip('configured to source changes using pubsub', function() {
|
||||||
|
before(function() {
|
||||||
|
var test = this;
|
||||||
|
var app = loopback({localRegistry: true});
|
||||||
|
var db = app.dataSource('ds', {connector: 'memory'});
|
||||||
|
var ps = app.dataSource('ps', {
|
||||||
|
host: 'localhost',
|
||||||
|
port: '12345',
|
||||||
|
connector: 'pubsub',
|
||||||
|
pubsubAdapter: 'mqtt'
|
||||||
|
});
|
||||||
|
this.Score = app.model('Score', {
|
||||||
|
dataSource: 'db',
|
||||||
|
changeDataSource: 'ps'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change', function(done) {
|
||||||
|
var Score = this.Score;
|
||||||
|
|
||||||
|
Score.createChangeStream(function(err, changes) {
|
||||||
|
changes.on('data', function(change) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -614,7 +614,8 @@ describe.onServer('Remote Methods', function() {
|
||||||
'destroyById',
|
'destroyById',
|
||||||
'removeById',
|
'removeById',
|
||||||
'count',
|
'count',
|
||||||
'prototype.updateAttributes'
|
'prototype.updateAttributes',
|
||||||
|
'createChangeStream'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue