UNPKG

nqm-minimongo

Version:

Client-side mongo database with server sync over http

849 lines (683 loc) 25 kB
_ = require 'lodash' chai = require 'chai' assert = chai.assert sinon = require 'sinon' lolex = require 'lolex' MemoryDb = require "../src/MemoryDb" HybridDb = require "../src/HybridDb" db_queries = require "./db_queries" # Note: Assumes local db is synchronous! fail = -> throw new Error("failed") describe 'HybridDb', -> before (done) -> @reset = (done) => @local = new MemoryDb() @remote = new MemoryDb() @hybrid = new HybridDb(@local, @remote) @local.addCollection("scratch") @lc = @local.scratch @remote.addCollection("scratch") @rc = @remote.scratch @hybrid.addCollection("scratch") @hc = @hybrid.scratch @col = @hc done() @reset(done) describe "passes queries", -> beforeEach (done) -> @reset(done) db_queries.call(this) context "resets each time", -> beforeEach (done) -> @reset(done) describe "interim:true (default)", -> it "find gives only one result if data unchanged", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:1) @rc.seed(_id:"2", a:2) calls = 0 @hc.find({}).fetch (data) -> calls += 1 assert.equal data.length, 2 assert.equal calls, 1 done() , fail it "find gives results twice if remote gives different answer", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.find({}).fetch (data) -> assert.equal data.length, 2 calls = calls + 1 if calls >= 2 done() , fail it "find gives results once if remote gives same answer with sort differences", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.find = -> return fetch: (success) -> success([{_id:"2", a:2}, {_id:"1", a:1}]) @hc.find({}).fetch (data) -> assert.equal data.length, 2 done() , fail it "local upserts are respected", (done) -> @lc.seed(_id:"1", a:1) @lc.upsert(_id:"2", a:2) @rc.seed(_id:"1", a:1) @rc.seed(_id:"2", a:4) @hc.findOne { _id: "2"}, (doc) -> assert.deepEqual doc, { _id: "2", a: 2 } done() , fail describe "cacheFind: true (default)", -> it "find performs full field remote queries", (done) -> @rc.seed(_id:"1", a:1, b:11) @rc.seed(_id:"2", a:2, b:12) @hc.find({}, { fields: { b:0 } }).fetch (data) => if data.length == 0 return assert.isUndefined data[0].b @lc.findOne { _id: "1" }, (doc) -> assert.equal doc.b, 11 done() it "caches remote data", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:2) calls = 0 @hc.find({}).fetch (data) => assert.equal data.length, 2 calls = calls + 1 # After second call, check that local collection has latest if calls == 2 @lc.find({}).fetch (data) -> assert.equal data.length, 2 assert.deepEqual _.pluck(data, 'a'), [3,2] done() describe "cacheFindOne: true (default)", -> it "findOne performs full field remote queries", (done) -> @rc.seed(_id:"1", a:1, b:11) @rc.seed(_id:"2", a:2, b:12) @hc.findOne { _id: "1" }, { fields: { b:0 } }, (doc) => assert.isUndefined doc.b @lc.findOne { _id: "1" }, (doc) -> assert.equal doc.b, 11 done() it "findOne gives results twice if remote gives different answer", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.findOne { _id: "1"}, (data) -> calls = calls + 1 if calls == 1 assert.deepEqual data, { _id : "1", a:1 } if calls >= 2 assert.deepEqual data, { _id : "1", a:3 } done() , fail it "findOne gives local results once if remote fails", (done) -> @lc.seed(_id:"1", a:1) @rc.findOne = (selector, options = {}, success, error) -> error(new Error("fail")) @rc.find = (selector, options) -> return { fetch: (success, error) -> error() } @hc.findOne { _id: "1"}, (data) -> assert.equal data.a, 1 done() , fail it "findOne gives local results selected not by _id once if remote fails", (done) -> @lc.seed(_id:"1", a:1) @rc.findOne = (selector, options = {}, success, error) -> error(new Error("fail")) @rc.find = (selector, options) -> return { fetch: (success, error) -> error() } @hc.findOne { a: 1 }, (data) -> assert.equal data.a, 1 done() , fail it "findOne gives local results once if remote fails", (done) -> called = 0 @rc.findOne = (selector, options = {}, success, error) -> called = called + 1 error(new Error("fail")) @rc.find = (selector, options) -> return { fetch: (success, error) -> called = called + 1 error() } @hc.findOne { _id: "xyz"}, (data) -> assert.equal data, null assert.equal called, 1 done() , fail it "findOne keeps local cache updated on remote change", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.findOne { _id: "1"}, (data) => calls = calls + 1 if calls == 1 assert.deepEqual data, { _id : "1", a:1 } if calls >= 2 assert.deepEqual data, { _id : "1", a:3 } @lc.find({}, {}).fetch (data) -> assert.deepEqual _.pluck(data, 'a'), [3,2] done() , fail describe "interim: false", -> it "find gives final results only", (done) -> @lc.upsert(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.find({}, {interim: false}).fetch (data) -> assert.equal data.length, 2 assert.equal data[0].a, 1 assert.equal data[1].a, 4 done() , fail describe "interim: false with timeout", -> beforeEach -> @clock = lolex.install() afterEach -> @clock.uninstall() it "find gives final results if in time", (done) -> @lc.upsert(_id:"1", a:1) @lc.seed(_id:"2", a:2) oldFind = @rc.find @rc.find = (where, params) => return { fetch: (success, error) => # Wait a bit @clock.tick(500) success([{ _id:"1", a:3 }, { _id: "2", a: 4}]) @clock.tick(1) } @hc.find({}, {interim: false, timeout: 1000 }).fetch (data) -> assert.equal data.length, 2 assert.equal data[0].a, 1 assert.equal data[1].a, 4 done() , fail it "find gives local results if out of time", (done) -> @lc.upsert(_id:"1", a:1) @lc.seed(_id:"2", a:2) oldFind = @rc.find @rc.find = (where, params) => return { fetch: (success, error) => # Wait a bit too long @clock.tick(1500) success([{ _id:"1", a:3 }, { _id: "2", a: 4}]) @clock.tick(1) } @hc.find({}, { interim: false, timeout: 1000 }).fetch (data) -> assert.equal data.length, 2 assert.equal data[0].a, 1 assert.equal data[1].a, 2 done() , fail it "find gives local results but still caches if out of time", (done) -> @lc.upsert(_id:"1", a:1) @lc.seed(_id:"2", a:2) oldFind = @rc.find @rc.find = (where, params) => return { fetch: (success, error) => # Wait a bit too long @clock.tick(1500) success([{ _id:"1", a:3 }, { _id: "2", a: 4}]) @clock.tick(2000) } @hc.find({}, { interim: false, timeout: 1000 }).fetch (data) => assert.equal data.length, 2 assert.equal data[0].a, 1 assert.equal data[1].a, 2 # Wait longer for remote to complete setTimeout () => @lc.find({}, {}).fetch (data) => assert.equal data.length, 2 assert.equal data[0].a, 1, "Should not change since upsert" assert.equal data[1].a, 4 done() , 1000 , fail it "find gives local results once if remote fails then out of time", (done) -> @lc.upsert(_id:"1", a:1) @lc.seed(_id:"2", a:2) oldFind = @rc.find @rc.find = (where, params) => return { fetch: (success, error) => error(new Error("Fail")) @clock.tick(1) } called = 0 @hc.find({}, { interim: false, timeout: 1000 }).fetch (data) => assert.equal data.length, 2 assert.equal data[0].a, 1 assert.equal data[1].a, 2 called += 1 # Wait a bit too long @clock.tick(1500) if called > 1 console.error "Fail! Called twice" assert.equal called, 1 done() , fail it "find gives local results once if out of time then remote fails", (done) -> @lc.upsert(_id:"1", a:1) @lc.seed(_id:"2", a:2) oldFind = @rc.find @rc.find = (where, params) => return { fetch: (success, error) => @clock.tick(1500) error(new Error("Fail")) } called = 0 @hc.find({}, { interim: false, timeout: 1000 }).fetch (data) => assert.equal data.length, 2 assert.equal data[0].a, 1 assert.equal data[1].a, 2 called += 1 if called > 1 console.error "Fail! Called twice" assert.equal called, 1 done() , fail describe "cacheFind: false", -> it "find performs partial field remote queries", (done) -> sinon.spy(@rc, "find") @rc.seed(_id:"1", a:1, b:11) @rc.seed(_id:"2", a:2, b:12) @hc.find({}, { fields: { b:0 }, cacheFind: false }).fetch (data) => if data.length == 0 return assert.isUndefined data[0].b assert.deepEqual @rc.find.firstCall.args[1].fields, { b:0 } @rc.find.restore() done() it "does not cache remote data", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:2) calls = 0 @hc.find({}, {cacheFind: false}).fetch (data) => assert.equal data.length, 2 calls = calls + 1 # After second call, check that local collection is unchanged if calls == 2 @lc.find({}).fetch (data) -> assert.equal data.length, 2 assert.deepEqual _.pluck(data, 'a'), [1,2] done() describe "cacheFindOne: false", -> it "findOne performs partial field remote queries", (done) -> sinon.spy(@rc, "find") @rc.seed(_id:"1", a:1, b:11) @rc.seed(_id:"2", a:2, b:12) @hc.findOne { _id: "1" }, { fields: { b:0 }, cacheFindOne: false }, (data) => if data == null return assert.isUndefined data.b assert.deepEqual @rc.find.getCall(0).args[1].fields, { b:0 } @rc.find.restore() done() context "shortcut: false (default)", -> it "findOne calls both local and remote", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.findOne { _id: "1" }, (data) -> calls += 1 if calls == 1 assert.deepEqual data, { _id : "1", a:1 } else assert.deepEqual data, { _id : "1", a:3 } done() , fail context "interim: false", -> it "findOne calls both local and remote", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) @hc.findOne { _id: "1" }, { interim: false }, (data) -> assert.deepEqual data, { _id : "1", a:3 } done() , fail it "findOne calls remote if not found", (done) -> @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.findOne { _id: "1"}, { shortcut: true }, (data) -> assert.deepEqual data, { _id : "1", a:3 } done() , fail context "shortcut: true", -> it "findOne only calls local if found", (done) -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.findOne { _id: "1" }, { shortcut: true }, (data) -> assert.deepEqual data, { _id : "1", a:1 } done() , fail it "findOne calls remote if not found", (done) -> @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) calls = 0 @hc.findOne { _id: "1"}, { shortcut: true }, (data) -> assert.deepEqual data, { _id : "1", a:3 } done() , fail context "cacheFind: false, interim: false", -> beforeEach -> @lc.seed(_id:"1", a:1) @lc.seed(_id:"2", a:2) @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) it "find only calls remote", (done) -> @hc.find({}, { cacheFind: false, interim: false }).fetch (data) -> assert.deepEqual _.pluck(data, 'a'), [3,4] done() it "find does not cache results", (done) -> @hc.find({}, { cacheFind: false, interim: false }).fetch (data) => @lc.find({}).fetch (data) => assert.deepEqual _.pluck(data, 'a'), [1,2] done() it "find falls back to local if remote fails", (done) -> @rc.find = (selector, options) -> return { fetch: (success, error) -> error() } @hc.find({}, { cacheFind: false, interim: false }).fetch (data) -> assert.deepEqual _.pluck(data, 'a'), [1,2] done() it "find errors if useLocalOnRemoteError:false if remote fails", (done) -> @rc.find = (selector, options) => return { fetch: (success, error) -> error() } @hc.find({}, { cacheFind: false, interim: false, useLocalOnRemoteError:false }).fetch (data) => assert.fail() , (err) -> done() it "find respects local upserts", (done) -> @lc.upsert({ _id:"1", a:9 }) @hc.find({}, { cacheFind: false, interim: false, sort: ['_id'] }).fetch (data) => assert.deepEqual _.pluck(data, 'a'), [9,4] done() it "find respects local removes", (done) -> @lc.remove("1") @hc.find({}, { cacheFind: false, interim: false }).fetch (data) -> assert.deepEqual _.pluck(data, 'a'), [4] done() it "upload applies pending upserts", (done) -> @lc.upsert(_id:"1", a:1) @lc.upsert(_id:"2", a:2) @hybrid.upload(=> @lc.pendingUpserts (data) => assert.equal data.length, 0 @rc.pendingUpserts (data) -> assert.deepEqual _.pluck(_.pluck(data, 'doc'), 'a'), [1,2] done() , fail) it "upload sorts pending upserts", (done) -> @lc.upsert(_id:"1", a:1, b: 2) @lc.upsert(_id:"2", a:2, b: 1) hybrid = new HybridDb(@local, @remote) hybrid.addCollection("scratch", { sortUpserts: (u1, u2) -> (if u1.b < u2.b then -1 else 1)}) upserts = [] @rc.upsert = (doc, base, success, error) => upserts.push(doc) success() hybrid.upload(=> @lc.pendingUpserts (data) => assert.equal data.length, 0 assert.deepEqual _.pluck(upserts, 'a'), [2, 1] done() , fail) it "does not resolve upsert if data changed, but changes base", (done) -> @lc.upsert(_id:"1", a:1) # Override pending upserts to change doc right before returning oldPendingUpserts = @lc.pendingUpserts @lc.pendingUpserts = (success) => oldPendingUpserts.call @lc, (upserts) => # Alter row @lc.upsert(_id:"1", a:2) success(upserts) @hybrid.upload(=> @lc.pendingUpserts (data) => assert.equal data.length, 1 assert.deepEqual data[0].doc, { _id:"1", a:2 } assert.deepEqual data[0].base, { _id:"1", a:1 } @rc.pendingUpserts (data) -> assert.deepEqual data[0].doc, { _id:"1", a:1 } assert.isNull data[0].base done() , fail) it "caches new upserted value", (done) -> @lc.upsert(_id:"1", a:1) # Override remote upsert to change returned doc @rc.upsert = (docs, bases, success) -> success(_id:"1", a:2) @hybrid.upload(=> @lc.pendingUpserts (data) => assert.equal data.length, 0 @lc.findOne {_id:"1"}, {}, (data) -> assert.deepEqual data, { _id:"1", a:2 } done() , fail) it "upload applies pending removes", (done) -> @lc.seed(_id:"1", a:1) @rc.seed(_id:"1", a:1) @hc.remove("1") @hybrid.upload(=> @lc.pendingRemoves (data) => assert.equal data.length, 0 @rc.pendingRemoves (data) -> assert.deepEqual data, ["1"] done() , fail) it "keeps upserts and deletes if failed to apply", (done) -> @lc.upsert(_id:"1", a:1) @lc.upsert(_id:"2", a:2) @lc.seed(_id:"3", a:3) @rc.seed(_id:"3", a:3) @hc.remove("3") @rc.upsert = (docs, bases, success, error) -> error(new Error("fail")) @rc.remove = (id, success, error) -> error(new Error("fail")) @hybrid.upload(-> assert.fail() , ()=> @lc.pendingUpserts (data) => assert.equal data.length, 2 @lc.pendingRemoves (data) -> assert.equal data.length, 1 assert.equal data[0], "3" done() ) it "removes upsert if fails with 410 (gone) and continue", (done) -> @lc.upsert(_id:"1", a:1) @rc.upsert = (docs, bases, success, error) -> error({ status: 410 }) @hybrid.upload => @lc.pendingUpserts (data) => assert.equal data.length, 0 @lc.pendingRemoves (data) => assert.equal data.length, 0 @lc.findOne { _id: "1"}, (data) -> assert.isNull data done() , fail , fail , fail , fail it "removes upsert if fails with 403 (permission) and fail", (done) -> @lc.upsert(_id:"1", a:1) @rc.upsert = (docs, bases, success, error) -> error({ status: 403 }) @hybrid.upload fail, => @lc.pendingUpserts (data) => assert.equal data.length, 0 @lc.pendingRemoves (data) => assert.equal data.length, 0 @lc.findOne { _id: "1"}, (data) -> assert.isNull data done() , fail , fail , fail it "removes document if remove fails with 403 (permission) and fail", (done) -> @lc.seed(_id:"1", a:1) @hc.remove("3") @rc.remove = (id, success, error) -> error({ status: 403 }) @hybrid.upload(-> assert.fail() , ()=> @lc.pendingUpserts (data) => assert.equal data.length, 0, "Should have zero upserts" @lc.pendingRemoves (data) => assert.equal data.length, 0, "Should have zero removes" @lc.findOne { _id: "1" }, (data) -> assert.equal data.a, 1 done() ) it "removes upsert if returns null", (done) -> @lc.upsert(_id:"1", a:1) @rc.upsert = (docs, bases, success, error) -> success(null) @hybrid.upload => @lc.pendingUpserts (data) => assert.equal data.length, 0 @lc.pendingRemoves (data) => assert.equal data.length, 0 @lc.findOne { _id: "1"}, (data) -> assert.isNull data done() , fail , fail , fail , fail it "upserts to local db", (done) -> @hc.upsert(_id:"1", a:1) @lc.pendingUpserts (data) -> assert.equal data.length, 1 done() it "upserts to local db with base version", (done) -> @hc.upsert({_id:"1", a:2}, {_id:"1", a:1}) @lc.pendingUpserts (data) -> assert.equal data.length, 1 assert.equal data[0].doc.a, 2 assert.equal data[0].base.a, 1 done() it "removes to local db", (done) -> @lc.seed(_id:"1", a:1) @hc.remove("1") @lc.pendingRemoves (data) -> assert.equal data.length, 1 done() context "cacheFind: false, interim: false", -> beforeEach -> @local = new MemoryDb() @remote = new MemoryDb() @hybrid = new HybridDb(@local, @remote) @local.addCollection("scratch") @lc = @local.scratch @remote.addCollection("scratch") @rc = @remote.scratch @hybrid.addCollection("scratch") @hc = @hybrid.scratch # Seed some remote data @rc.seed(_id:"1", a:3) @rc.seed(_id:"2", a:4) it "find uses remote", (done) -> @hc.find({}, { cacheFind: false, interim: false }).fetch (data) => assert.deepEqual _.pluck(data, 'a'), [3,4] done() it "find does not cache results", (done) -> @hc.find({}, { cacheFind: false, interim: false }).fetch (data) => @lc.find({}).fetch (data) => assert.equal data.length, 0 done() it "find respects local upserts", (done) -> @lc.upsert({ _id:"1", a:9 }) @hc.find({}, { cacheFind: false, interim: false, sort: ['_id'] }).fetch (data) => assert.deepEqual _.pluck(data, 'a'), [9,4] done() it "find respects local removes", (done) -> @lc.remove("1") @hc.find({}, { cacheFind: false, interim: false }).fetch (data) => assert.deepEqual _.pluck(data, 'a'), [4] done() it "findOne without _id selector uses remote", (done) -> @hc.findOne {}, { cacheFindOne: false, interim: false, sort: ['_id'] }, (data) => assert.deepEqual data, { _id:"1", a:3 } done() it "findOne without _id selector respects local upsert", (done) -> @lc.upsert({ _id:"1", a:9 }) @hc.findOne {}, { cacheFindOne: false, interim: false, sort: ['_id'] }, (data) => assert.deepEqual data, { _id:"1", a:9 } done() it "findOne without _id selector respects local remove", (done) -> @lc.remove("1") @hc.findOne {}, { cacheFindOne: false, sort: ['_id'] }, (data) => assert.deepEqual data, { _id: "2", a: 4 } done() it "findOne with _id selector uses remote", (done) -> @hc.findOne { _id: "1" }, { cacheFindOne: false, sort: ['_id'] }, (data) => assert.deepEqual data, { _id:"1", a:3 } done() it "findOne with _id selector respects local upsert", (done) -> @lc.upsert({ _id:"1", a:9 }) @hc.findOne { _id: "1" }, { cacheFindOne: false, interim: false, sort: ['_id'] }, (data) => assert.deepEqual data, { _id:"1", a:9 } done() it "findOne with _id selector respects local remove", (done) -> @lc.remove("1") @hc.findOne { _id: "1" }, { cacheFindOne: false, interim: false, sort: ['_id'] }, (data) => assert.isNull data done() # Only use this test if cacheUpsert is used in the future # it "upload success removes from local", (done) -> # @lc.upsert({ _id:"1", a:9 }) # @hybrid.upload => # # Not pending locally # @lc.pendingRemoves (data) => # assert.equal data.length, 0 # # Pending remotely # @rc.pendingUpserts (data) => # assert.deepEqual _.pluck(_.pluck(data, 'doc'), "a"), [9] # # Not cached locally # @lc.find({}).fetch (data) => # assert.equal data.length, 0 # done() # , fail # , fail # , fail