nqm-minimongo
Version:
Client-side mongo database with server sync over http
849 lines (683 loc) • 25 kB
text/coffeescript
_ = 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