diff --git a/index.js b/index.js index df8411d..c283685 100644 --- a/index.js +++ b/index.js @@ -439,11 +439,15 @@ ShareDbMongo.prototype.getOpsToSnapshot = function(collectionName, id, from, sna var err = ShareDbMongo.missingLastOperationError(collectionName, id); return callback(err); } + var options = Object.assign({}, options); var to = null; this._getOps(collectionName, id, from, to, options, function(err, ops) { if (err) return callback(err); var filtered = getLinkedOps(ops, null, snapshot._opLink); - var err = checkOpsFrom(collectionName, id, filtered, from); + var err = null; + if (!options.ignoreMissingOps) { + err = checkOpsFrom(collectionName, id, filtered, from); + } if (err) return callback(err); callback(null, filtered); }); @@ -451,6 +455,7 @@ ShareDbMongo.prototype.getOpsToSnapshot = function(collectionName, id, from, sna ShareDbMongo.prototype.getOps = function(collectionName, id, from, to, options, callback) { var self = this; + var options = Object.assign({}, options); this._getOpLink(collectionName, id, to, options, function(err, opLink) { if (err) return callback(err); // We need to fetch slightly more ops than requested in order to work backwards along @@ -464,10 +469,14 @@ ShareDbMongo.prototype.getOps = function(collectionName, id, from, to, options, if (err) return callback(err); if (self.getOpsWithoutStrictLinking) fetchOpsTo = opLink._v; } + self._getOps(collectionName, id, from, fetchOpsTo, options, function(err, ops) { if (err) return callback(err); var filtered = filterOps(ops, opLink, to); - var err = checkOpsFrom(collectionName, id, filtered, from); + var err = null; + if (!options.ignoreMissingOps) { + err = checkOpsFrom(collectionName, id, filtered, from); + } if (err) return callback(err); callback(null, filtered); }); diff --git a/test/test_get_ops.js b/test/test_get_ops.js new file mode 100644 index 0000000..7510a9d --- /dev/null +++ b/test/test_get_ops.js @@ -0,0 +1,135 @@ +var expect = require('chai').expect; +var ShareDbMongo = require('..'); +var sinon = require('sinon'); + +var mongoUrl = process.env.TEST_MONGO_URL || 'mongodb://localhost:27017/test'; + +function create(options, callback) { + var opts = Object.assign({ + mongoOptions: {}, + getOpsWithoutStrictLinking: true + }, options); + var db = new ShareDbMongo(mongoUrl, opts); + db.getDbs(function(err, mongo) { + if (err) return callback(err); + mongo.dropDatabase(function(err) { + if (err) return callback(err); + callback(null, db, mongo); + }); + }); +}; + +// loop thru strict linking options +[true, false].forEach(function(strictLinkingOption) { + describe('getOps with strict linking ' + strictLinkingOption, function() { + beforeEach(function(done) { + var self = this; + create( + {getOpsWithoutStrictLinking: strictLinkingOption}, + function(err, db, mongo) { + if (err) return done(err); + self.db = db; + self.mongo = mongo; + done(); + }); + }); + + afterEach(function(done) { + this.db.close(done); + }); + + describe('a chain of ops', function() { + var db; + var mongo; + var id; + var collection; + + beforeEach(function(done) { + db = this.db; + mongo = this.mongo; + id = 'document1'; + collection = 'testcollection'; + + sinon.spy(db, '_getOps'); + sinon.spy(db, '_getSnapshotOpLink'); + + var ops = [ + {v: 0, create: {}}, + {v: 1, p: ['foo'], oi: 'bar'}, + {v: 2, p: ['foo'], oi: 'baz'}, + {v: 3, p: ['foo'], oi: 'qux'} + ]; + + commitOpChain(db, mongo, collection, id, ops, function(error) { + if (error) done(error); + mongo.collection('o_' + collection).deleteOne({v: 1}, done); + }); + }); + + it('fetches ops 2-3 without fetching all ops', function(done) { + db.getOps(collection, id, 2, 4, null, function(error, ops) { + if (error) return done(error); + expect(ops.length).to.equal(2); + expect(ops[0].v).to.equal(2); + expect(ops[1].v).to.equal(3); + done(); + }); + }); + + it('default option errors when missing ops', function(done) { + db.getOps(collection, id, 0, 4, null, function(error) { + expect(error.code).to.equal(5103); + expect(error.message).to.equal('Missing ops from requested version testcollection.document1 0'); + done(); + }); + }); + + it('ignoreMissingOps option returns ops up to the first missing op', function(done) { + db.getOps(collection, id, 0, 4, {ignoreMissingOps: true}, function(error, ops) { + if (error) return done(error); + expect(ops.length).to.equal(2); + expect(ops[0].v).to.equal(2); + expect(ops[1].v).to.equal(3); + done(); + }); + }); + + it('getOpsToSnapshot ignoreMissingOps option returns ops up to the first missing op', function(done) { + db.getSnapshot(collection, id, {$submit: true}, null, function(error, snapshot) { + if (error) done(error); + db.getOpsToSnapshot(collection, id, 0, snapshot, {ignoreMissingOps: true}, function(error, ops) { + if (error) return done(error); + expect(ops.length).to.equal(2); + expect(ops[0].v).to.equal(2); + expect(ops[1].v).to.equal(3); + done(); + }); + }); + }); + }); + }); +}); + +function commitOpChain(db, mongo, collection, id, ops, previousOpId, version, callback) { + if (typeof previousOpId === 'function') { + callback = previousOpId; + previousOpId = undefined; + version = 0; + } + + ops = ops.slice(); + var op = ops.shift(); + + if (!op) { + return callback(); + } + + var snapshot = {id: id, v: version + 1, type: 'json0', data: {}, m: null, _opLink: previousOpId}; + db.commit(collection, id, op, snapshot, null, function(error) { + if (error) return callback(error); + mongo.collection('o_' + collection).find({d: id, v: version}).next(function(error, op) { + if (error) return callback(error); + commitOpChain(db, mongo, collection, id, ops, (op ? op._id : null), ++version, callback); + }); + }); +}