mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 15:56:10 +08:00
es6(db): Convert the ORM specs to ES2016
This commit is contained in:
parent
51f9001c21
commit
ed8b0b222e
|
@ -1,32 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
Model = require '../src/flux/models/model'
|
||||
Attributes = require('../src/flux/attributes').default
|
||||
DatabaseObjectRegistry = require('../src/database-object-registry').default
|
||||
|
||||
class GoodTest extends Model
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
"foo": Attributes.String
|
||||
modelKey: 'foo'
|
||||
jsonKey: 'foo'
|
||||
|
||||
describe 'DatabaseObjectRegistry', ->
|
||||
beforeEach ->
|
||||
DatabaseObjectRegistry.unregister("GoodTest")
|
||||
|
||||
it "can register constructors", ->
|
||||
testFn = -> GoodTest
|
||||
expect( -> DatabaseObjectRegistry.register("GoodTest", testFn)).not.toThrow()
|
||||
expect(DatabaseObjectRegistry.get("GoodTest")).toBe GoodTest
|
||||
|
||||
it "Tests if a constructor is in the registry", ->
|
||||
DatabaseObjectRegistry.register("GoodTest", -> GoodTest)
|
||||
expect(DatabaseObjectRegistry.isInRegistry("GoodTest")).toBe true
|
||||
|
||||
it "deserializes the objects for a constructor", ->
|
||||
DatabaseObjectRegistry.register("GoodTest", -> GoodTest)
|
||||
obj = DatabaseObjectRegistry.deserialize("GoodTest", foo: "bar")
|
||||
expect(obj instanceof GoodTest).toBe true
|
||||
expect(obj.foo).toBe "bar"
|
||||
|
||||
it "throws an error if the object can't be deserialized", ->
|
||||
expect( -> DatabaseObjectRegistry.deserialize("GoodTest", foo: "bar")).toThrow()
|
40
spec/database-object-registry-spec.es6
Normal file
40
spec/database-object-registry-spec.es6
Normal file
|
@ -0,0 +1,40 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import _ from 'underscore';
|
||||
import Model from '../src/flux/models/model';
|
||||
import Attributes from '../src/flux/attributes';
|
||||
import DatabaseObjectRegistry from '../src/database-object-registry';
|
||||
|
||||
class GoodTest extends Model {
|
||||
static attributes = _.extend({}, Model.attributes, {
|
||||
"foo": Attributes.String({
|
||||
modelKey: 'foo',
|
||||
jsonKey: 'foo',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
describe('DatabaseObjectRegistry', function DatabaseObjectRegistrySpecs() {
|
||||
beforeEach(() => DatabaseObjectRegistry.unregister("GoodTest"));
|
||||
|
||||
it("can register constructors", () => {
|
||||
const testFn = () => GoodTest;
|
||||
expect(() => DatabaseObjectRegistry.register("GoodTest", testFn)).not.toThrow();
|
||||
expect(DatabaseObjectRegistry.get("GoodTest")).toBe(GoodTest);
|
||||
});
|
||||
|
||||
it("Tests if a constructor is in the registry", () => {
|
||||
DatabaseObjectRegistry.register("GoodTest", () => GoodTest);
|
||||
expect(DatabaseObjectRegistry.isInRegistry("GoodTest")).toBe(true);
|
||||
});
|
||||
|
||||
it("deserializes the objects for a constructor", () => {
|
||||
DatabaseObjectRegistry.register("GoodTest", () => GoodTest);
|
||||
const obj = DatabaseObjectRegistry.deserialize("GoodTest", {foo: "bar"});
|
||||
expect(obj instanceof GoodTest).toBe(true);
|
||||
expect(obj.foo).toBe("bar");
|
||||
});
|
||||
|
||||
it("throws an error if the object can't be deserialized", () =>
|
||||
expect(() => DatabaseObjectRegistry.deserialize("GoodTest", {foo: "bar"})).toThrow()
|
||||
);
|
||||
});
|
|
@ -1,239 +0,0 @@
|
|||
Model = require '../../src/flux/models/model'
|
||||
Utils = require '../../src/flux/models/utils'
|
||||
Attributes = require('../../src/flux/attributes').default
|
||||
{isTempId} = require '../../src/flux/models/utils'
|
||||
_ = require 'underscore'
|
||||
|
||||
describe "Model", ->
|
||||
describe "constructor", ->
|
||||
it "should accept a hash of attributes and assign them to the new Model", ->
|
||||
attrs =
|
||||
id: "A",
|
||||
accountId: "B"
|
||||
m = new Model(attrs)
|
||||
expect(m.id).toBe(attrs.id)
|
||||
expect(m.accountId).toBe(attrs.accountId)
|
||||
|
||||
it "by default assigns things passed into the id constructor to the serverId", ->
|
||||
attrs =
|
||||
id: "A",
|
||||
m = new Model(attrs)
|
||||
expect(m.serverId).toBe(attrs.id)
|
||||
|
||||
it "by default assigns values passed into the id constructor that look like localIds to be a localID", ->
|
||||
attrs =
|
||||
id: "A",
|
||||
m = new Model(attrs)
|
||||
expect(m.serverId).toBe(attrs.id)
|
||||
|
||||
it "assigns serverIds and clientIds", ->
|
||||
attrs =
|
||||
clientId: "local-A",
|
||||
serverId: "A",
|
||||
m = new Model(attrs)
|
||||
expect(m.serverId).toBe(attrs.serverId)
|
||||
expect(m.clientId).toBe(attrs.clientId)
|
||||
expect(m.id).toBe(attrs.serverId)
|
||||
|
||||
it "throws an error if you attempt to manually assign the id", ->
|
||||
m = new Model(id: "foo")
|
||||
expect( -> m.id = "bar" ).toThrow()
|
||||
|
||||
it "automatically assigns a clientId (and id) to the model if no id is provided", ->
|
||||
m = new Model
|
||||
expect(Utils.isTempId(m.id)).toBe true
|
||||
expect(Utils.isTempId(m.clientId)).toBe true
|
||||
expect(m.serverId).toBeUndefined()
|
||||
|
||||
describe "attributes", ->
|
||||
it "should return the attributes of the class EXCEPT the id field", ->
|
||||
m = new Model()
|
||||
retAttrs = _.clone(m.constructor.attributes)
|
||||
delete retAttrs["id"]
|
||||
expect(m.attributes()).toEqual(retAttrs)
|
||||
|
||||
describe "clone", ->
|
||||
it "should return a deep copy of the object", ->
|
||||
class SubSubmodel extends Model
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
'value': Attributes.Number
|
||||
modelKey: 'value'
|
||||
jsonKey: 'value'
|
||||
|
||||
class Submodel extends Model
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
'testNumber': Attributes.Number
|
||||
modelKey: 'testNumber'
|
||||
jsonKey: 'test_number'
|
||||
'testArray': Attributes.Collection
|
||||
itemClass: SubSubmodel
|
||||
modelKey: 'testArray'
|
||||
jsonKey: 'test_array'
|
||||
|
||||
old = new Submodel(testNumber: 4, testArray: [new SubSubmodel(value: 2), new SubSubmodel(value: 6)])
|
||||
clone = old.clone()
|
||||
|
||||
# Check entire trees are equivalent
|
||||
expect(old.toJSON()).toEqual(clone.toJSON())
|
||||
# Check object identity has changed
|
||||
expect(old.constructor.name).toEqual(clone.constructor.name)
|
||||
expect(old.testArray).not.toBe(clone.testArray)
|
||||
# Check classes
|
||||
expect(old.testArray[0]).not.toBe(clone.testArray[0])
|
||||
expect(old.testArray[0].constructor.name).toEqual(clone.testArray[0].constructor.name)
|
||||
|
||||
describe "fromJSON", ->
|
||||
beforeEach ->
|
||||
class SubmodelItem extends Model
|
||||
|
||||
class Submodel extends Model
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
'testNumber': Attributes.Number
|
||||
modelKey: 'testNumber'
|
||||
jsonKey: 'test_number'
|
||||
'testBoolean': Attributes.Boolean
|
||||
modelKey: 'testBoolean'
|
||||
jsonKey: 'test_boolean'
|
||||
'testCollection': Attributes.Collection
|
||||
modelKey: 'testCollection'
|
||||
jsonKey: 'test_collection'
|
||||
itemClass: SubmodelItem
|
||||
'testJoinedData': Attributes.JoinedData
|
||||
modelKey: 'testJoinedData'
|
||||
jsonKey: 'test_joined_data'
|
||||
|
||||
@json =
|
||||
'id': '1234'
|
||||
'test_number': 4
|
||||
'test_boolean': true
|
||||
'daysOld': 4
|
||||
'account_id': 'bla'
|
||||
@m = new Submodel
|
||||
|
||||
it "should assign attribute values by calling through to attribute fromJSON functions", ->
|
||||
spyOn(Model.attributes.accountId, 'fromJSON').andCallFake (json) ->
|
||||
'inflated value!'
|
||||
@m.fromJSON(@json)
|
||||
expect(Model.attributes.accountId.fromJSON.callCount).toBe 1
|
||||
expect(@m.accountId).toBe('inflated value!')
|
||||
|
||||
it "should not touch attributes that are missing in the json", ->
|
||||
@m.fromJSON(@json)
|
||||
expect(@m.object).toBe(undefined)
|
||||
|
||||
@m.object = 'abc'
|
||||
@m.fromJSON(@json)
|
||||
expect(@m.object).toBe('abc')
|
||||
|
||||
it "should not do anything with extra JSON keys", ->
|
||||
@m.fromJSON(@json)
|
||||
expect(@m.daysOld).toBe(undefined)
|
||||
|
||||
it "should maintain empty string as empty strings", ->
|
||||
expect(@m.accountId).toBe(undefined)
|
||||
@m.fromJSON({account_id: ''})
|
||||
expect(@m.accountId).toBe('')
|
||||
|
||||
describe "Attributes.Number", ->
|
||||
it "should read number attributes and coerce them to numeric values", ->
|
||||
@m.fromJSON('test_number': 4)
|
||||
expect(@m.testNumber).toBe(4)
|
||||
|
||||
@m.fromJSON('test_number': '4')
|
||||
expect(@m.testNumber).toBe(4)
|
||||
|
||||
@m.fromJSON('test_number': 'lolz')
|
||||
expect(@m.testNumber).toBe(null)
|
||||
|
||||
@m.fromJSON('test_number': 0)
|
||||
expect(@m.testNumber).toBe(0)
|
||||
|
||||
describe "Attributes.JoinedData", ->
|
||||
it "should read joined data attributes and coerce them to string values", ->
|
||||
@m.fromJSON('test_joined_data': null)
|
||||
expect(@m.testJoinedData).toBe(null)
|
||||
|
||||
@m.fromJSON('test_joined_data': '')
|
||||
expect(@m.testJoinedData).toBe('')
|
||||
|
||||
@m.fromJSON('test_joined_data': 'lolz')
|
||||
expect(@m.testJoinedData).toBe('lolz')
|
||||
|
||||
describe "Attributes.Collection", ->
|
||||
it "should parse and inflate items", ->
|
||||
@m.fromJSON('test_collection': [{id: '123'}])
|
||||
expect(@m.testCollection.length).toBe(1)
|
||||
expect(@m.testCollection[0].id).toBe('123')
|
||||
expect(@m.testCollection[0].constructor.name).toBe('SubmodelItem')
|
||||
|
||||
it "should be fine with malformed arrays", ->
|
||||
@m.fromJSON('test_collection': [null])
|
||||
expect(@m.testCollection.length).toBe(0)
|
||||
@m.fromJSON('test_collection': [])
|
||||
expect(@m.testCollection.length).toBe(0)
|
||||
@m.fromJSON('test_collection': null)
|
||||
expect(@m.testCollection.length).toBe(0)
|
||||
|
||||
describe "Attributes.Boolean", ->
|
||||
it "should read `true` or true and coerce everything else to false", ->
|
||||
@m.fromJSON('test_boolean': true)
|
||||
expect(@m.testBoolean).toBe(true)
|
||||
|
||||
@m.fromJSON('test_boolean': 'true')
|
||||
expect(@m.testBoolean).toBe(true)
|
||||
|
||||
@m.fromJSON('test_boolean': 4)
|
||||
expect(@m.testBoolean).toBe(false)
|
||||
|
||||
@m.fromJSON('test_boolean': '4')
|
||||
expect(@m.testBoolean).toBe(false)
|
||||
|
||||
@m.fromJSON('test_boolean': false)
|
||||
expect(@m.testBoolean).toBe(false)
|
||||
|
||||
@m.fromJSON('test_boolean': 0)
|
||||
expect(@m.testBoolean).toBe(false)
|
||||
|
||||
@m.fromJSON('test_boolean': null)
|
||||
expect(@m.testBoolean).toBe(false)
|
||||
|
||||
describe "toJSON", ->
|
||||
beforeEach ->
|
||||
@model = new Model
|
||||
id: "1234",
|
||||
accountId: "ACD"
|
||||
|
||||
it "should return a JSON object and call attribute toJSON functions to map values", ->
|
||||
spyOn(Model.attributes.accountId, 'toJSON').andCallFake (json) ->
|
||||
'inflated value!'
|
||||
|
||||
json = @model.toJSON()
|
||||
expect(json instanceof Object).toBe(true)
|
||||
expect(json.id).toBe('1234')
|
||||
expect(json.account_id).toBe('inflated value!')
|
||||
|
||||
it "should surface any exception one of the attribute toJSON functions raises", ->
|
||||
spyOn(Model.attributes.accountId, 'toJSON').andCallFake (json) ->
|
||||
throw new Error("Can't convert value into JSON format")
|
||||
expect(-> @model.toJSON()).toThrow()
|
||||
|
||||
describe "matches", ->
|
||||
beforeEach ->
|
||||
@model = new Model
|
||||
id: "1234",
|
||||
accountId: "ACD"
|
||||
|
||||
@truthyMatcher =
|
||||
evaluate: (model) -> true
|
||||
@falsyMatcher =
|
||||
evaluate: (model) -> false
|
||||
|
||||
it "should run the matchers and return true iff all matchers pass", ->
|
||||
expect(@model.matches([@truthyMatcher, @truthyMatcher])).toBe(true)
|
||||
expect(@model.matches([@truthyMatcher, @falsyMatcher])).toBe(false)
|
||||
expect(@model.matches([@falsyMatcher, @truthyMatcher])).toBe(false)
|
||||
|
||||
it "should pass itself as an argument to the matchers", ->
|
||||
spyOn(@truthyMatcher, 'evaluate').andCallFake (param) =>
|
||||
expect(param).toBe(@model)
|
||||
@model.matches([@truthyMatcher])
|
294
spec/models/model-spec.es6
Normal file
294
spec/models/model-spec.es6
Normal file
|
@ -0,0 +1,294 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import Model from '../../src/flux/models/model';
|
||||
import Utils from '../../src/flux/models/utils';
|
||||
import Attributes from '../../src/flux/attributes';
|
||||
import _ from 'underscore';
|
||||
|
||||
describe("Model", function modelSpecs() {
|
||||
describe("constructor", () => {
|
||||
it("should accept a hash of attributes and assign them to the new Model", () => {
|
||||
const attrs = {
|
||||
id: "A",
|
||||
accountId: "B",
|
||||
};
|
||||
const m = new Model(attrs);
|
||||
expect(m.id).toBe(attrs.id);
|
||||
return expect(m.accountId).toBe(attrs.accountId);
|
||||
});
|
||||
|
||||
it("by default assigns things passed into the id constructor to the serverId", () => {
|
||||
const attrs = {id: "A"};
|
||||
const m = new Model(attrs);
|
||||
return expect(m.serverId).toBe(attrs.id);
|
||||
});
|
||||
|
||||
it("by default assigns values passed into the id constructor that look like localIds to be a localID", () => {
|
||||
const attrs = {id: "A"};
|
||||
const m = new Model(attrs);
|
||||
return expect(m.serverId).toBe(attrs.id);
|
||||
});
|
||||
|
||||
it("assigns serverIds and clientIds", () => {
|
||||
const attrs = {
|
||||
clientId: "local-A",
|
||||
serverId: "A",
|
||||
};
|
||||
const m = new Model(attrs);
|
||||
expect(m.serverId).toBe(attrs.serverId);
|
||||
expect(m.clientId).toBe(attrs.clientId);
|
||||
return expect(m.id).toBe(attrs.serverId);
|
||||
});
|
||||
|
||||
it("throws an error if you attempt to manually assign the id", () => {
|
||||
const m = new Model({id: "foo"});
|
||||
return expect(() => { m.id = "bar" }).toThrow();
|
||||
});
|
||||
|
||||
return it("automatically assigns a clientId (and id) to the model if no id is provided", () => {
|
||||
const m = new Model;
|
||||
expect(Utils.isTempId(m.id)).toBe(true);
|
||||
expect(Utils.isTempId(m.clientId)).toBe(true);
|
||||
return expect(m.serverId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("attributes", () =>
|
||||
it("should return the attributes of the class EXCEPT the id field", () => {
|
||||
const m = new Model();
|
||||
const retAttrs = _.clone(m.constructor.attributes);
|
||||
delete retAttrs.id;
|
||||
return expect(m.attributes()).toEqual(retAttrs);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("clone", () =>
|
||||
it("should return a deep copy of the object", () => {
|
||||
class SubSubmodel extends Model {
|
||||
static attributes = Object.assign({}, Model.attributes, {
|
||||
'value': Attributes.Number({
|
||||
modelKey: 'value',
|
||||
jsonKey: 'value',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
class Submodel extends Model {
|
||||
static attributes = Object.assign({}, Model.attributes, {
|
||||
'testNumber': Attributes.Number({
|
||||
modelKey: 'testNumber',
|
||||
jsonKey: 'test_number',
|
||||
}),
|
||||
'testArray': Attributes.Collection({
|
||||
itemClass: SubSubmodel,
|
||||
modelKey: 'testArray',
|
||||
jsonKey: 'test_array',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const old = new Submodel({testNumber: 4, testArray: [new SubSubmodel({value: 2}), new SubSubmodel({value: 6})]});
|
||||
const clone = old.clone();
|
||||
|
||||
// Check entire trees are equivalent
|
||||
expect(old.toJSON()).toEqual(clone.toJSON());
|
||||
// Check object identity has changed
|
||||
expect(old.constructor.name).toEqual(clone.constructor.name);
|
||||
expect(old.testArray).not.toBe(clone.testArray);
|
||||
// Check classes
|
||||
expect(old.testArray[0]).not.toBe(clone.testArray[0]);
|
||||
return expect(old.testArray[0].constructor.name).toEqual(clone.testArray[0].constructor.name);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("fromJSON", () => {
|
||||
beforeEach(() => {
|
||||
class SubmodelItem extends Model {}
|
||||
|
||||
class Submodel extends Model {
|
||||
static attributes = Object.assign({}, Model.attributes, {
|
||||
'testNumber': Attributes.Number({
|
||||
modelKey: 'testNumber',
|
||||
jsonKey: 'test_number',
|
||||
}),
|
||||
'testBoolean': Attributes.Boolean({
|
||||
modelKey: 'testBoolean',
|
||||
jsonKey: 'test_boolean',
|
||||
}),
|
||||
'testCollection': Attributes.Collection({
|
||||
modelKey: 'testCollection',
|
||||
jsonKey: 'test_collection',
|
||||
itemClass: SubmodelItem,
|
||||
}),
|
||||
'testJoinedData': Attributes.JoinedData({
|
||||
modelKey: 'testJoinedData',
|
||||
jsonKey: 'test_joined_data',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
this.json = {
|
||||
'id': '1234',
|
||||
'test_number': 4,
|
||||
'test_boolean': true,
|
||||
'daysOld': 4,
|
||||
'account_id': 'bla',
|
||||
};
|
||||
this.m = new Submodel;
|
||||
});
|
||||
|
||||
it("should assign attribute values by calling through to attribute fromJSON functions", () => {
|
||||
spyOn(Model.attributes.accountId, 'fromJSON').andCallFake(() => 'inflated value!');
|
||||
this.m.fromJSON(this.json);
|
||||
expect(Model.attributes.accountId.fromJSON.callCount).toBe(1);
|
||||
return expect(this.m.accountId).toBe('inflated value!');
|
||||
});
|
||||
|
||||
it("should not touch attributes that are missing in the json", () => {
|
||||
this.m.fromJSON(this.json);
|
||||
expect(this.m.object).toBe(undefined);
|
||||
|
||||
this.m.object = 'abc';
|
||||
this.m.fromJSON(this.json);
|
||||
return expect(this.m.object).toBe('abc');
|
||||
});
|
||||
|
||||
it("should not do anything with extra JSON keys", () => {
|
||||
this.m.fromJSON(this.json);
|
||||
return expect(this.m.daysOld).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should maintain empty string as empty strings", () => {
|
||||
expect(this.m.accountId).toBe(undefined);
|
||||
this.m.fromJSON({account_id: ''});
|
||||
return expect(this.m.accountId).toBe('');
|
||||
});
|
||||
|
||||
describe("Attributes.Number", () =>
|
||||
it("should read number attributes and coerce them to numeric values", () => {
|
||||
this.m.fromJSON({'test_number': 4});
|
||||
expect(this.m.testNumber).toBe(4);
|
||||
|
||||
this.m.fromJSON({'test_number': '4'});
|
||||
expect(this.m.testNumber).toBe(4);
|
||||
|
||||
this.m.fromJSON({'test_number': 'lolz'});
|
||||
expect(this.m.testNumber).toBe(null);
|
||||
|
||||
this.m.fromJSON({'test_number': 0});
|
||||
return expect(this.m.testNumber).toBe(0);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("Attributes.JoinedData", () =>
|
||||
it("should read joined data attributes and coerce them to string values", () => {
|
||||
this.m.fromJSON({'test_joined_data': null});
|
||||
expect(this.m.testJoinedData).toBe(null);
|
||||
|
||||
this.m.fromJSON({'test_joined_data': ''});
|
||||
expect(this.m.testJoinedData).toBe('');
|
||||
|
||||
this.m.fromJSON({'test_joined_data': 'lolz'});
|
||||
return expect(this.m.testJoinedData).toBe('lolz');
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("Attributes.Collection", () => {
|
||||
it("should parse and inflate items", () => {
|
||||
this.m.fromJSON({'test_collection': [{id: '123'}]});
|
||||
expect(this.m.testCollection.length).toBe(1);
|
||||
expect(this.m.testCollection[0].id).toBe('123');
|
||||
return expect(this.m.testCollection[0].constructor.name).toBe('SubmodelItem');
|
||||
});
|
||||
|
||||
return it("should be fine with malformed arrays", () => {
|
||||
this.m.fromJSON({'test_collection': [null]});
|
||||
expect(this.m.testCollection.length).toBe(0);
|
||||
this.m.fromJSON({'test_collection': []});
|
||||
expect(this.m.testCollection.length).toBe(0);
|
||||
this.m.fromJSON({'test_collection': null});
|
||||
return expect(this.m.testCollection.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("Attributes.Boolean", () =>
|
||||
it("should read `true` or true and coerce everything else to false", () => {
|
||||
this.m.fromJSON({'test_boolean': true});
|
||||
expect(this.m.testBoolean).toBe(true);
|
||||
|
||||
this.m.fromJSON({'test_boolean': 'true'});
|
||||
expect(this.m.testBoolean).toBe(true);
|
||||
|
||||
this.m.fromJSON({'test_boolean': 4});
|
||||
expect(this.m.testBoolean).toBe(false);
|
||||
|
||||
this.m.fromJSON({'test_boolean': '4'});
|
||||
expect(this.m.testBoolean).toBe(false);
|
||||
|
||||
this.m.fromJSON({'test_boolean': false});
|
||||
expect(this.m.testBoolean).toBe(false);
|
||||
|
||||
this.m.fromJSON({'test_boolean': 0});
|
||||
expect(this.m.testBoolean).toBe(false);
|
||||
|
||||
this.m.fromJSON({'test_boolean': null});
|
||||
return expect(this.m.testBoolean).toBe(false);
|
||||
})
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
describe("toJSON", () => {
|
||||
beforeEach(() => {
|
||||
this.model = new Model({
|
||||
id: "1234",
|
||||
accountId: "ACD",
|
||||
});
|
||||
return;
|
||||
});
|
||||
|
||||
it("should return a JSON object and call attribute toJSON functions to map values", () => {
|
||||
spyOn(Model.attributes.accountId, 'toJSON').andCallFake(() => 'inflated value!');
|
||||
|
||||
const json = this.model.toJSON();
|
||||
expect(json instanceof Object).toBe(true);
|
||||
expect(json.id).toBe('1234');
|
||||
return expect(json.account_id).toBe('inflated value!');
|
||||
});
|
||||
|
||||
return it("should surface any exception one of the attribute toJSON functions raises", () => {
|
||||
spyOn(Model.attributes.accountId, 'toJSON').andCallFake(() => {
|
||||
throw new Error("Can't convert value into JSON format");
|
||||
});
|
||||
return expect(() => { return this.model.toJSON(); }).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
return describe("matches", () => {
|
||||
beforeEach(() => {
|
||||
this.model = new Model({
|
||||
id: "1234",
|
||||
accountId: "ACD",
|
||||
});
|
||||
|
||||
this.truthyMatcher = {evaluate() { return true; }};
|
||||
this.falsyMatcher = {evaluate() { return false; }};
|
||||
});
|
||||
|
||||
it("should run the matchers and return true iff all matchers pass", () => {
|
||||
expect(this.model.matches([this.truthyMatcher, this.truthyMatcher])).toBe(true);
|
||||
expect(this.model.matches([this.truthyMatcher, this.falsyMatcher])).toBe(false);
|
||||
return expect(this.model.matches([this.falsyMatcher, this.truthyMatcher])).toBe(false);
|
||||
});
|
||||
|
||||
return it("should pass itself as an argument to the matchers", () => {
|
||||
spyOn(this.truthyMatcher, 'evaluate').andCallFake(param => {
|
||||
return expect(param).toBe(this.model);
|
||||
});
|
||||
return this.model.matches([this.truthyMatcher]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,130 +0,0 @@
|
|||
MutableQueryResultSet = require('../../src/flux/models/mutable-query-result-set').default
|
||||
QueryRange = require('../../src/flux/models/query-range').default
|
||||
_ = require 'underscore'
|
||||
|
||||
describe "MutableQueryResultSet", ->
|
||||
describe "clipToRange", ->
|
||||
it "should do nothing if the clipping range is infinite", ->
|
||||
set = new MutableQueryResultSet(_ids: ['A','B','C','D','E'], _offset: 5)
|
||||
beforeRange = set.range()
|
||||
set.clipToRange(QueryRange.infinite())
|
||||
afterRange = set.range()
|
||||
|
||||
expect(beforeRange.isEqual(afterRange)).toBe(true)
|
||||
|
||||
it "should correctly trim the result set 5-10 to the clipping range 2-9", ->
|
||||
set = new MutableQueryResultSet(_ids: ['A','B','C','D','E'], _offset: 5)
|
||||
expect(set.range().isEqual(new QueryRange(offset: 5, limit: 5))).toBe(true)
|
||||
set.clipToRange(new QueryRange(offset: 2, limit: 7))
|
||||
expect(set.range().isEqual(new QueryRange(offset: 5, limit: 4))).toBe(true)
|
||||
expect(set.ids()).toEqual(['A','B','C','D'])
|
||||
|
||||
it "should correctly trim the result set 5-10 to the clipping range 5-10", ->
|
||||
set = new MutableQueryResultSet(_ids: ['A','B','C','D','E'], _offset: 5)
|
||||
set.clipToRange(new QueryRange(start: 5, end: 10))
|
||||
expect(set.range().isEqual(new QueryRange(start: 5, end: 10))).toBe(true)
|
||||
expect(set.ids()).toEqual(['A','B','C','D','E'])
|
||||
|
||||
it "should correctly trim the result set 5-10 to the clipping range 6", ->
|
||||
set = new MutableQueryResultSet(_ids: ['A','B','C','D','E'], _offset: 5)
|
||||
set.clipToRange(new QueryRange(offset: 6, limit: 1))
|
||||
expect(set.range().isEqual(new QueryRange(offset: 6, limit: 1))).toBe(true)
|
||||
expect(set.ids()).toEqual(['B'])
|
||||
|
||||
it "should correctly trim the result set 5-10 to the clipping range 100-200", ->
|
||||
set = new MutableQueryResultSet(_ids: ['A','B','C','D','E'], _offset: 5)
|
||||
set.clipToRange(new QueryRange(start: 100, end: 200))
|
||||
expect(set.range().isEqual(new QueryRange(start: 100, end: 100))).toBe(true)
|
||||
expect(set.ids()).toEqual([])
|
||||
|
||||
it "should correctly trim the result set 5-10 to the clipping range 0-2", ->
|
||||
set = new MutableQueryResultSet(_ids: ['A','B','C','D','E'], _offset: 5)
|
||||
set.clipToRange(new QueryRange(offset: 0, limit: 2))
|
||||
expect(set.range().isEqual(new QueryRange(offset: 5, limit: 0))).toBe(true)
|
||||
expect(set.ids()).toEqual([])
|
||||
|
||||
it "should trim the models cache to remove models no longer needed", ->
|
||||
set = new MutableQueryResultSet
|
||||
_ids: ['A','B','C','D','E'],
|
||||
_offset: 5
|
||||
_modelsHash: {
|
||||
'A-local': {id: 'A', clientId: 'A-local'},
|
||||
'A': {id: 'A', clientId: 'A-local'},
|
||||
'B-local': {id: 'B', clientId: 'B-local'},
|
||||
'B': {id: 'B', clientId: 'B-local'},
|
||||
'C-local': {id: 'C', clientId: 'C-local'},
|
||||
'C': {id: 'C', clientId: 'C-local'},
|
||||
'D-local': {id: 'D', clientId: 'D-local'},
|
||||
'D': {id: 'D', clientId: 'D-local'},
|
||||
'E-local': {id: 'E', clientId: 'E-local'},
|
||||
'E': {id: 'E', clientId: 'E-local'}
|
||||
}
|
||||
|
||||
set.clipToRange(new QueryRange(start: 5, end: 8))
|
||||
expect(set._modelsHash).toEqual({
|
||||
'A-local': {id: 'A', clientId: 'A-local'},
|
||||
'A': {id: 'A', clientId: 'A-local'},
|
||||
'B-local': {id: 'B', clientId: 'B-local'},
|
||||
'B': {id: 'B', clientId: 'B-local'},
|
||||
'C-local': {id: 'C', clientId: 'C-local'},
|
||||
'C': {id: 'C', clientId: 'C-local'},
|
||||
})
|
||||
|
||||
describe "addIdsInRange", ->
|
||||
describe "when the set is currently empty", ->
|
||||
it "should set the result set to the provided one", ->
|
||||
@set = new MutableQueryResultSet()
|
||||
@set.addIdsInRange(['B','C','D'], new QueryRange(start: 1, end: 4))
|
||||
expect(@set.ids()).toEqual(['B','C','D'])
|
||||
expect(@set.range().isEqual(new QueryRange(start: 1, end: 4))).toBe(true)
|
||||
|
||||
describe "when the set has existing values", ->
|
||||
beforeEach ->
|
||||
@set = new MutableQueryResultSet
|
||||
_ids: ['A','B','C','D','E'],
|
||||
_offset: 5
|
||||
_modelsHash: {'A': {id: 'A'}, 'B': {id: 'B'}, 'C': {id: 'C'}, 'D': {id: 'D'}, 'E': {id: 'E'}}
|
||||
|
||||
it "should throw an exception if the range provided doesn't intersect (trailing)", ->
|
||||
expect =>
|
||||
@set.addIdsInRange(['G', 'H', 'I'], new QueryRange(offset: 11, limit: 3))
|
||||
.toThrow()
|
||||
expect =>
|
||||
@set.addIdsInRange(['F', 'G', 'H'], new QueryRange(offset: 10, limit: 3))
|
||||
.not.toThrow()
|
||||
|
||||
it "should throw an exception if the range provided doesn't intersect (leading)", ->
|
||||
expect =>
|
||||
@set.addIdsInRange(['0', '1', '2'], new QueryRange(offset: 1, limit: 3))
|
||||
.toThrow()
|
||||
expect =>
|
||||
@set.addIdsInRange(['0', '1', '2'], new QueryRange(offset: 2, limit: 3))
|
||||
.not.toThrow()
|
||||
|
||||
it "should work if the IDs array is shorter than the result range they represent (addition)", ->
|
||||
@set.addIdsInRange(['F', 'G', 'H'], new QueryRange(offset: 10, limit: 5))
|
||||
expect(@set.ids()).toEqual(['A','B','C','D','E', 'F', 'G', 'H'])
|
||||
|
||||
it "should work if the IDs array is shorter than the result range they represent (replacement)", ->
|
||||
@set.addIdsInRange(['A', 'B', 'C'], new QueryRange(offset: 5, limit: 5))
|
||||
expect(@set.ids()).toEqual(['A','B','C'])
|
||||
|
||||
it "should correctly add ids (trailing) and update the offset", ->
|
||||
@set.addIdsInRange(['F', 'G', 'H'], new QueryRange(offset: 10, limit: 3))
|
||||
expect(@set.ids()).toEqual(['A','B','C','D','E','F','G','H'])
|
||||
expect(@set.range().offset).toEqual(5)
|
||||
|
||||
it "should correctly add ids (leading) and update the offset", ->
|
||||
@set.addIdsInRange(['0', '1', '2'], new QueryRange(offset: 2, limit: 3))
|
||||
expect(@set.ids()).toEqual(['0', '1', '2', 'A','B','C','D','E'])
|
||||
expect(@set.range().offset).toEqual(2)
|
||||
|
||||
it "should correctly add ids (middle) and update the offset", ->
|
||||
@set.addIdsInRange(['B-new', 'C-new', 'D-new'], new QueryRange(offset: 6, limit: 3))
|
||||
expect(@set.ids()).toEqual(['A', 'B-new', 'C-new', 'D-new','E'])
|
||||
expect(@set.range().offset).toEqual(5)
|
||||
|
||||
it "should correctly add ids (middle+trailing) and update the offset", ->
|
||||
@set.addIdsInRange(['D-new', 'E-new', 'F-new'], new QueryRange(offset: 8, limit: 3))
|
||||
expect(@set.ids()).toEqual(['A', 'B', 'C', 'D-new','E-new', 'F-new'])
|
||||
expect(@set.range().offset).toEqual(5)
|
156
spec/models/mutable-query-result-set-spec.es6
Normal file
156
spec/models/mutable-query-result-set-spec.es6
Normal file
|
@ -0,0 +1,156 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import MutableQueryResultSet from '../../src/flux/models/mutable-query-result-set';
|
||||
import QueryRange from '../../src/flux/models/query-range';
|
||||
|
||||
describe("MutableQueryResultSet", function MutableQueryResultSetSpecs() {
|
||||
describe("clipToRange", () => {
|
||||
it("should do nothing if the clipping range is infinite", () => {
|
||||
const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});
|
||||
const beforeRange = set.range();
|
||||
set.clipToRange(QueryRange.infinite());
|
||||
const afterRange = set.range();
|
||||
|
||||
expect(beforeRange.isEqual(afterRange)).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly trim the result set 5-10 to the clipping range 2-9", () => {
|
||||
const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});
|
||||
expect(set.range().isEqual(new QueryRange({offset: 5, limit: 5}))).toBe(true);
|
||||
set.clipToRange(new QueryRange({offset: 2, limit: 7}));
|
||||
expect(set.range().isEqual(new QueryRange({offset: 5, limit: 4}))).toBe(true);
|
||||
expect(set.ids()).toEqual(['A', 'B', 'C', 'D']);
|
||||
});
|
||||
|
||||
it("should correctly trim the result set 5-10 to the clipping range 5-10", () => {
|
||||
const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});
|
||||
set.clipToRange(new QueryRange({start: 5, end: 10}));
|
||||
expect(set.range().isEqual(new QueryRange({start: 5, end: 10}))).toBe(true);
|
||||
expect(set.ids()).toEqual(['A', 'B', 'C', 'D', 'E']);
|
||||
});
|
||||
|
||||
it("should correctly trim the result set 5-10 to the clipping range 6", () => {
|
||||
const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});
|
||||
set.clipToRange(new QueryRange({offset: 6, limit: 1}));
|
||||
expect(set.range().isEqual(new QueryRange({offset: 6, limit: 1}))).toBe(true);
|
||||
expect(set.ids()).toEqual(['B']);
|
||||
});
|
||||
|
||||
it("should correctly trim the result set 5-10 to the clipping range 100-200", () => {
|
||||
const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});
|
||||
set.clipToRange(new QueryRange({start: 100, end: 200}));
|
||||
expect(set.range().isEqual(new QueryRange({start: 100, end: 100}))).toBe(true);
|
||||
expect(set.ids()).toEqual([]);
|
||||
});
|
||||
|
||||
it("should correctly trim the result set 5-10 to the clipping range 0-2", () => {
|
||||
const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});
|
||||
set.clipToRange(new QueryRange({offset: 0, limit: 2}));
|
||||
expect(set.range().isEqual(new QueryRange({offset: 5, limit: 0}))).toBe(true);
|
||||
expect(set.ids()).toEqual([]);
|
||||
});
|
||||
|
||||
it("should trim the models cache to remove models no longer needed", () => {
|
||||
const set = new MutableQueryResultSet({
|
||||
_ids: ['A', 'B', 'C', 'D', 'E'],
|
||||
_offset: 5,
|
||||
_modelsHash: {
|
||||
'A-local': {id: 'A', clientId: 'A-local'},
|
||||
'A': {id: 'A', clientId: 'A-local'},
|
||||
'B-local': {id: 'B', clientId: 'B-local'},
|
||||
'B': {id: 'B', clientId: 'B-local'},
|
||||
'C-local': {id: 'C', clientId: 'C-local'},
|
||||
'C': {id: 'C', clientId: 'C-local'},
|
||||
'D-local': {id: 'D', clientId: 'D-local'},
|
||||
'D': {id: 'D', clientId: 'D-local'},
|
||||
'E-local': {id: 'E', clientId: 'E-local'},
|
||||
'E': {id: 'E', clientId: 'E-local'},
|
||||
}});
|
||||
|
||||
set.clipToRange(new QueryRange({start: 5, end: 8}));
|
||||
expect(set._modelsHash).toEqual({
|
||||
'A-local': {id: 'A', clientId: 'A-local'},
|
||||
'A': {id: 'A', clientId: 'A-local'},
|
||||
'B-local': {id: 'B', clientId: 'B-local'},
|
||||
'B': {id: 'B', clientId: 'B-local'},
|
||||
'C-local': {id: 'C', clientId: 'C-local'},
|
||||
'C': {id: 'C', clientId: 'C-local'},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("addIdsInRange", () => {
|
||||
describe("when the set is currently empty", () =>
|
||||
it("should set the result set to the provided one", () => {
|
||||
this.set = new MutableQueryResultSet();
|
||||
this.set.addIdsInRange(['B', 'C', 'D'], new QueryRange({start: 1, end: 4}));
|
||||
expect(this.set.ids()).toEqual(['B', 'C', 'D']);
|
||||
expect(this.set.range().isEqual(new QueryRange({start: 1, end: 4}))).toBe(true);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("when the set has existing values", () => {
|
||||
beforeEach(() => {
|
||||
this.set = new MutableQueryResultSet({
|
||||
_ids: ['A', 'B', 'C', 'D', 'E'],
|
||||
_offset: 5,
|
||||
_modelsHash: {'A': {id: 'A'}, 'B': {id: 'B'}, 'C': {id: 'C'}, 'D': {id: 'D'}, 'E': {id: 'E'}},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw an exception if the range provided doesn't intersect (trailing)", () => {
|
||||
expect(() => {
|
||||
this.set.addIdsInRange(['G', 'H', 'I'], new QueryRange({offset: 11, limit: 3}));
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
this.set.addIdsInRange(['F', 'G', 'H'], new QueryRange({offset: 10, limit: 3}));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw an exception if the range provided doesn't intersect (leading)", () => {
|
||||
expect(() => {
|
||||
this.set.addIdsInRange(['0', '1', '2'], new QueryRange({offset: 1, limit: 3}));
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
this.set.addIdsInRange(['0', '1', '2'], new QueryRange({offset: 2, limit: 3}));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should work if the IDs array is shorter than the result range they represent (addition)", () => {
|
||||
this.set.addIdsInRange(['F', 'G', 'H'], new QueryRange({offset: 10, limit: 5}));
|
||||
expect(this.set.ids()).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);
|
||||
});
|
||||
|
||||
it("should work if the IDs array is shorter than the result range they represent (replacement)", () => {
|
||||
this.set.addIdsInRange(['A', 'B', 'C'], new QueryRange({offset: 5, limit: 5}));
|
||||
expect(this.set.ids()).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it("should correctly add ids (trailing) and update the offset", () => {
|
||||
this.set.addIdsInRange(['F', 'G', 'H'], new QueryRange({offset: 10, limit: 3}));
|
||||
expect(this.set.ids()).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);
|
||||
expect(this.set.range().offset).toEqual(5);
|
||||
});
|
||||
|
||||
it("should correctly add ids (leading) and update the offset", () => {
|
||||
this.set.addIdsInRange(['0', '1', '2'], new QueryRange({offset: 2, limit: 3}));
|
||||
expect(this.set.ids()).toEqual(['0', '1', '2', 'A', 'B', 'C', 'D', 'E']);
|
||||
expect(this.set.range().offset).toEqual(2);
|
||||
});
|
||||
|
||||
it("should correctly add ids (middle) and update the offset", () => {
|
||||
this.set.addIdsInRange(['B-new', 'C-new', 'D-new'], new QueryRange({offset: 6, limit: 3}));
|
||||
expect(this.set.ids()).toEqual(['A', 'B-new', 'C-new', 'D-new', 'E']);
|
||||
expect(this.set.range().offset).toEqual(5);
|
||||
});
|
||||
|
||||
it("should correctly add ids (middle+trailing) and update the offset", () => {
|
||||
this.set.addIdsInRange(['D-new', 'E-new', 'F-new'], new QueryRange({offset: 8, limit: 3}));
|
||||
expect(this.set.ids()).toEqual(['A', 'B', 'C', 'D-new', 'E-new', 'F-new']);
|
||||
expect(this.set.range().offset).toEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
QueryRange = require('../../src/flux/models/query-range').default
|
||||
|
||||
describe "QueryRange", ->
|
||||
describe "@infinite", ->
|
||||
it "should return a query range with a null limit and offset", ->
|
||||
infinite = QueryRange.infinite()
|
||||
expect(infinite.limit).toBe(null)
|
||||
expect(infinite.offset).toBe(null)
|
||||
|
||||
describe "@rangesBySubtracting", ->
|
||||
it "should throw an exception if either range is infinite", ->
|
||||
infinite = QueryRange.infinite()
|
||||
expect ->
|
||||
QueryRange.rangesBySubtracting(infinite, new QueryRange({offset: 0, limit: 10}))
|
||||
.toThrow()
|
||||
expect ->
|
||||
QueryRange.rangesBySubtracting(new QueryRange({offset: 0, limit: 10}), infinite)
|
||||
.toThrow()
|
||||
|
||||
it "should return one or more ranges created by punching the provided range", ->
|
||||
test = ({a, b, result}) ->
|
||||
expect(QueryRange.rangesBySubtracting(a, b)).toEqual(result)
|
||||
test
|
||||
a: new QueryRange(offset: 0, limit: 10),
|
||||
b: new QueryRange(offset: 3, limit: 3),
|
||||
result: [new QueryRange(offset: 0, limit: 3), new QueryRange(offset: 6, limit: 4)]
|
||||
test
|
||||
a: new QueryRange(offset: 0, limit: 10),
|
||||
b: new QueryRange(offset: 3, limit: 10),
|
||||
result: [new QueryRange(offset: 0, limit: 3)]
|
||||
test
|
||||
a: new QueryRange(offset: 0, limit: 10),
|
||||
b: new QueryRange(offset: 0, limit: 10),
|
||||
result: []
|
||||
test
|
||||
a: new QueryRange(offset: 5, limit: 10),
|
||||
b: new QueryRange(offset: 0, limit: 4),
|
||||
result: [new QueryRange(offset: 5, limit: 10)]
|
||||
test
|
||||
a: new QueryRange(offset: 5, limit: 10),
|
||||
b: new QueryRange(offset: 0, limit: 8),
|
||||
result: [new QueryRange(offset: 8, limit: 7)]
|
||||
|
||||
describe "isInfinite", ->
|
||||
it "should return true for an infinite range, false otherwise", ->
|
||||
infinite = QueryRange.infinite()
|
||||
expect(infinite.isInfinite()).toBe(true)
|
||||
expect(new QueryRange(offset:0, limit:4).isInfinite()).toBe(false)
|
||||
|
||||
describe "start", ->
|
||||
it "should be an alias for offset", ->
|
||||
expect((new QueryRange(offset:3, limit:4)).start).toBe(3)
|
||||
|
||||
describe "end", ->
|
||||
it "should be offset + limit", ->
|
||||
expect((new QueryRange(offset:3, limit:4)).end).toBe(7)
|
||||
|
||||
describe "isContiguousWith", ->
|
||||
it "should return true if either range is infinite", ->
|
||||
a = new QueryRange(offset:3, limit:4)
|
||||
expect(a.isContiguousWith(QueryRange.infinite())).toBe(true)
|
||||
expect(QueryRange.infinite().isContiguousWith(a)).toBe(true)
|
||||
|
||||
it "should return true if the ranges intersect or touch, false otherwise", ->
|
||||
a = new QueryRange(offset:3, limit:4)
|
||||
b = new QueryRange(offset:0, limit:2)
|
||||
c = new QueryRange(offset:0, limit:3)
|
||||
d = new QueryRange(offset:7, limit:10)
|
||||
e = new QueryRange(offset:8, limit:10)
|
||||
|
||||
# True
|
||||
|
||||
expect(a.isContiguousWith(d)).toBe(true)
|
||||
expect(d.isContiguousWith(a)).toBe(true)
|
||||
|
||||
expect(a.isContiguousWith(c)).toBe(true)
|
||||
expect(c.isContiguousWith(a)).toBe(true)
|
||||
|
||||
# False
|
||||
|
||||
expect(a.isContiguousWith(b)).toBe(false)
|
||||
expect(b.isContiguousWith(a)).toBe(false)
|
||||
|
||||
expect(a.isContiguousWith(e)).toBe(false)
|
||||
expect(e.isContiguousWith(a)).toBe(false)
|
||||
|
||||
expect(b.isContiguousWith(e)).toBe(false)
|
||||
expect(e.isContiguousWith(b)).toBe(false)
|
109
spec/models/query-range-spec.es6
Normal file
109
spec/models/query-range-spec.es6
Normal file
|
@ -0,0 +1,109 @@
|
|||
import QueryRange from '../../src/flux/models/query-range';
|
||||
|
||||
describe("QueryRange", function QueryRangeSpecs() {
|
||||
describe("@infinite", () =>
|
||||
it("should return a query range with a null limit and offset", () => {
|
||||
const infinite = QueryRange.infinite();
|
||||
expect(infinite.limit).toBe(null);
|
||||
expect(infinite.offset).toBe(null);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("@rangesBySubtracting", () => {
|
||||
it("should throw an exception if either range is infinite", () => {
|
||||
const infinite = QueryRange.infinite();
|
||||
|
||||
expect(() =>
|
||||
QueryRange.rangesBySubtracting(infinite, new QueryRange({offset: 0, limit: 10}))
|
||||
).toThrow();
|
||||
|
||||
expect(() =>
|
||||
QueryRange.rangesBySubtracting(new QueryRange({offset: 0, limit: 10}), infinite)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("should return one or more ranges created by punching the provided range", () => {
|
||||
const test = ({a, b, result}) => expect(QueryRange.rangesBySubtracting(a, b)).toEqual(result);
|
||||
test({
|
||||
a: new QueryRange({offset: 0, limit: 10}),
|
||||
b: new QueryRange({offset: 3, limit: 3}),
|
||||
result: [new QueryRange({offset: 0, limit: 3}), new QueryRange({offset: 6, limit: 4})]});
|
||||
|
||||
test({
|
||||
a: new QueryRange({offset: 0, limit: 10}),
|
||||
b: new QueryRange({offset: 3, limit: 10}),
|
||||
result: [new QueryRange({offset: 0, limit: 3})]});
|
||||
|
||||
test({
|
||||
a: new QueryRange({offset: 0, limit: 10}),
|
||||
b: new QueryRange({offset: 0, limit: 10}),
|
||||
result: []});
|
||||
|
||||
test({
|
||||
a: new QueryRange({offset: 5, limit: 10}),
|
||||
b: new QueryRange({offset: 0, limit: 4}),
|
||||
result: [new QueryRange({offset: 5, limit: 10})]});
|
||||
|
||||
test({
|
||||
a: new QueryRange({offset: 5, limit: 10}),
|
||||
b: new QueryRange({offset: 0, limit: 8}),
|
||||
result: [new QueryRange({offset: 8, limit: 7})]});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInfinite", () =>
|
||||
it("should return true for an infinite range, false otherwise", () => {
|
||||
const infinite = QueryRange.infinite();
|
||||
expect(infinite.isInfinite()).toBe(true);
|
||||
expect(new QueryRange({offset: 0, limit: 4}).isInfinite()).toBe(false);
|
||||
})
|
||||
);
|
||||
|
||||
describe("start", () =>
|
||||
it("should be an alias for offset", () =>
|
||||
expect((new QueryRange({offset: 3, limit: 4})).start).toBe(3)
|
||||
)
|
||||
);
|
||||
|
||||
describe("end", () =>
|
||||
it("should be offset + limit", () =>
|
||||
expect((new QueryRange({offset: 3, limit: 4})).end).toBe(7)
|
||||
)
|
||||
);
|
||||
|
||||
describe("isContiguousWith", () => {
|
||||
it("should return true if either range is infinite", () => {
|
||||
const a = new QueryRange({offset: 3, limit: 4});
|
||||
expect(a.isContiguousWith(QueryRange.infinite())).toBe(true);
|
||||
expect(QueryRange.infinite().isContiguousWith(a)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true if the ranges intersect or touch, false otherwise", () => {
|
||||
const a = new QueryRange({offset: 3, limit: 4});
|
||||
const b = new QueryRange({offset: 0, limit: 2});
|
||||
const c = new QueryRange({offset: 0, limit: 3});
|
||||
const d = new QueryRange({offset: 7, limit: 10});
|
||||
const e = new QueryRange({offset: 8, limit: 10});
|
||||
|
||||
// True
|
||||
|
||||
expect(a.isContiguousWith(d)).toBe(true);
|
||||
expect(d.isContiguousWith(a)).toBe(true);
|
||||
|
||||
expect(a.isContiguousWith(c)).toBe(true);
|
||||
expect(c.isContiguousWith(a)).toBe(true);
|
||||
|
||||
// False
|
||||
|
||||
expect(a.isContiguousWith(b)).toBe(false);
|
||||
expect(b.isContiguousWith(a)).toBe(false);
|
||||
|
||||
expect(a.isContiguousWith(e)).toBe(false);
|
||||
expect(e.isContiguousWith(a)).toBe(false);
|
||||
|
||||
expect(b.isContiguousWith(e)).toBe(false);
|
||||
expect(e.isContiguousWith(b)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,188 +0,0 @@
|
|||
ModelQuery = require('../../src/flux/models/query').default
|
||||
{Matcher} = require('../../src/flux/attributes').default
|
||||
Message = require('../../src/flux/models/message').default
|
||||
Thread = require('../../src/flux/models/thread').default
|
||||
Account = require('../../src/flux/models/account').default
|
||||
|
||||
describe "ModelQuery", ->
|
||||
beforeEach ->
|
||||
@db = {}
|
||||
|
||||
describe "where", ->
|
||||
beforeEach ->
|
||||
@q = new ModelQuery(Thread, @db)
|
||||
@m1 = Thread.attributes.id.equal(4)
|
||||
@m2 = Thread.attributes.categories.contains('category-id')
|
||||
|
||||
it "should accept an array of Matcher objects", ->
|
||||
@q.where([@m1,@m2])
|
||||
expect(@q._matchers.length).toBe(2)
|
||||
expect(@q._matchers[0]).toBe(@m1)
|
||||
expect(@q._matchers[1]).toBe(@m2)
|
||||
|
||||
it "should accept a single Matcher object", ->
|
||||
@q.where(@m1)
|
||||
expect(@q._matchers.length).toBe(1)
|
||||
expect(@q._matchers[0]).toBe(@m1)
|
||||
|
||||
it "should append to any existing where clauses", ->
|
||||
@q.where(@m1)
|
||||
@q.where(@m2)
|
||||
expect(@q._matchers.length).toBe(2)
|
||||
expect(@q._matchers[0]).toBe(@m1)
|
||||
expect(@q._matchers[1]).toBe(@m2)
|
||||
|
||||
it "should accept a shorthand format", ->
|
||||
@q.where({id: 4, lastMessageReceivedTimestamp: 1234})
|
||||
expect(@q._matchers.length).toBe(2)
|
||||
expect(@q._matchers[0].attr.modelKey).toBe('id')
|
||||
expect(@q._matchers[0].comparator).toBe('=')
|
||||
expect(@q._matchers[0].val).toBe(4)
|
||||
|
||||
it "should return the query so it can be chained", ->
|
||||
expect(@q.where({id: 4})).toBe(@q)
|
||||
|
||||
it "should immediately raise an exception if an un-queryable attribute is specified", ->
|
||||
expect(-> @q.where({snippet: 'My Snippet'})).toThrow()
|
||||
|
||||
it "should immediately raise an exception if a non-existent attribute is specified", ->
|
||||
expect(-> @q.where({looksLikeADuck: 'of course'})).toThrow()
|
||||
|
||||
describe "order", ->
|
||||
beforeEach ->
|
||||
@q = new ModelQuery(Thread, @db)
|
||||
@o1 = Thread.attributes.lastMessageReceivedTimestamp.descending()
|
||||
@o2 = Thread.attributes.subject.descending()
|
||||
|
||||
it "should accept an array of SortOrders", ->
|
||||
@q.order([@o1,@o2])
|
||||
expect(@q._orders.length).toBe(2)
|
||||
|
||||
it "should accept a single SortOrder object", ->
|
||||
@q.order(@o2)
|
||||
expect(@q._orders.length).toBe(1)
|
||||
|
||||
it "should extend any existing ordering", ->
|
||||
@q.order(@o1)
|
||||
@q.order(@o2)
|
||||
expect(@q._orders.length).toBe(2)
|
||||
expect(@q._orders[0]).toBe(@o1)
|
||||
expect(@q._orders[1]).toBe(@o2)
|
||||
|
||||
it "should return the query so it can be chained", ->
|
||||
expect(@q.order(@o2)).toBe(@q)
|
||||
|
||||
describe "include", ->
|
||||
beforeEach ->
|
||||
@q = new ModelQuery(Message, @db)
|
||||
|
||||
it "should throw an exception if the attribute is not a joined data attribute", ->
|
||||
expect( =>
|
||||
@q.include(Message.attributes.unread)
|
||||
).toThrow()
|
||||
|
||||
it "should add the provided property to the list of joined properties", ->
|
||||
expect(@q._includeJoinedData).toEqual([])
|
||||
@q.include(Message.attributes.body)
|
||||
expect(@q._includeJoinedData).toEqual([Message.attributes.body])
|
||||
|
||||
describe "includeAll", ->
|
||||
beforeEach ->
|
||||
@q = new ModelQuery(Message, @db)
|
||||
|
||||
it "should add all the JoinedData attributes of the class", ->
|
||||
expect(@q._includeJoinedData).toEqual([])
|
||||
@q.includeAll()
|
||||
expect(@q._includeJoinedData).toEqual([Message.attributes.body])
|
||||
|
||||
describe "response formatting", ->
|
||||
it "should always return a Number for counts", ->
|
||||
q = new ModelQuery(Message, @db)
|
||||
q.where({accountId: 'abcd'}).count()
|
||||
|
||||
raw = [{count:"12"}]
|
||||
expect(q.formatResult(q.inflateResult(raw))).toBe(12)
|
||||
|
||||
describe "sql", ->
|
||||
beforeEach ->
|
||||
@runScenario = (klass, scenario) ->
|
||||
q = new ModelQuery(klass, @db)
|
||||
Matcher.muid = 1
|
||||
scenario.builder(q)
|
||||
expect(q.sql().trim()).toBe(scenario.sql.trim())
|
||||
|
||||
it "should finalize the query so no further changes can be made", ->
|
||||
q = new ModelQuery(Account, @db)
|
||||
spyOn(q, 'finalize')
|
||||
q.sql()
|
||||
expect(q.finalize).toHaveBeenCalled()
|
||||
|
||||
it "should correctly generate queries with multiple where clauses", ->
|
||||
@runScenario Account,
|
||||
builder: (q) -> q.where({emailAddress: 'ben@nylas.com'}).where({id: 2})
|
||||
sql: "SELECT `Account`.`data` FROM `Account` \
|
||||
WHERE `Account`.`email_address` = 'ben@nylas.com' AND `Account`.`id` = 2"
|
||||
|
||||
it "should correctly escape single quotes with more double single quotes (LIKE)", ->
|
||||
@runScenario Account,
|
||||
builder: (q) -> q.where(Account.attributes.emailAddress.like("you're"))
|
||||
sql: "SELECT `Account`.`data` FROM `Account` WHERE `Account`.`email_address` like '%you''re%'"
|
||||
|
||||
it "should correctly escape single quotes with more double single quotes (equal)", ->
|
||||
@runScenario Account,
|
||||
builder: (q) -> q.where(Account.attributes.emailAddress.equal("you're"))
|
||||
sql: "SELECT `Account`.`data` FROM `Account` WHERE `Account`.`email_address` = 'you''re'"
|
||||
|
||||
it "should correctly generate COUNT queries", ->
|
||||
@runScenario Thread,
|
||||
builder: (q) -> q.where({accountId: 'abcd'}).count()
|
||||
sql: "SELECT COUNT(*) as count FROM `Thread` \
|
||||
WHERE `Thread`.`account_id` = 'abcd' "
|
||||
|
||||
it "should correctly generate LIMIT 1 queries for single items", ->
|
||||
@runScenario Thread,
|
||||
builder: (q) -> q.where({accountId: 'abcd'}).one()
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` \
|
||||
WHERE `Thread`.`account_id` = 'abcd' \
|
||||
ORDER BY `Thread`.`last_message_received_timestamp` DESC LIMIT 1"
|
||||
|
||||
it "should correctly generate `contains` queries using JOINS", ->
|
||||
@runScenario Thread,
|
||||
builder: (q) -> q.where(Thread.attributes.categories.contains('category-id')).where({id: '1234'})
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` \
|
||||
INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` \
|
||||
WHERE `M1`.`value` = 'category-id' AND `Thread`.`id` = '1234' \
|
||||
ORDER BY `Thread`.`last_message_received_timestamp` DESC"
|
||||
|
||||
@runScenario Thread,
|
||||
builder: (q) -> q.where([Thread.attributes.categories.contains('l-1'), Thread.attributes.categories.contains('l-2')])
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` \
|
||||
INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` \
|
||||
INNER JOIN `ThreadCategory` AS `M2` ON `M2`.`id` = `Thread`.`id` \
|
||||
WHERE `M1`.`value` = 'l-1' AND `M2`.`value` = 'l-2' \
|
||||
ORDER BY `Thread`.`last_message_received_timestamp` DESC"
|
||||
|
||||
it "should correctly generate queries with the class's naturalSortOrder when one is available and no other orders are provided", ->
|
||||
@runScenario Thread,
|
||||
builder: (q) -> q.where({accountId: 'abcd'})
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` \
|
||||
WHERE `Thread`.`account_id` = 'abcd' \
|
||||
ORDER BY `Thread`.`last_message_received_timestamp` DESC"
|
||||
|
||||
@runScenario Thread,
|
||||
builder: (q) -> q.where({accountId: 'abcd'}).order(Thread.attributes.lastMessageReceivedTimestamp.ascending())
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` \
|
||||
WHERE `Thread`.`account_id` = 'abcd' \
|
||||
ORDER BY `Thread`.`last_message_received_timestamp` ASC"
|
||||
|
||||
@runScenario Account,
|
||||
builder: (q) -> q.where({id: 'abcd'})
|
||||
sql: "SELECT `Account`.`data` FROM `Account` \
|
||||
WHERE `Account`.`id` = 'abcd' "
|
||||
|
||||
it "should correctly generate queries requesting joined data attributes", ->
|
||||
@runScenario Message,
|
||||
builder: (q) -> q.where({id: '1234'}).include(Message.attributes.body)
|
||||
sql: "SELECT `Message`.`data`, IFNULL(`MessageBody`.`value`, '!NULLVALUE!') AS `body` \
|
||||
FROM `Message` LEFT OUTER JOIN `MessageBody` ON `MessageBody`.`id` = `Message`.`id` \
|
||||
WHERE `Message`.`id` = '1234' ORDER BY `Message`.`date` ASC"
|
255
spec/models/query-spec.es6
Normal file
255
spec/models/query-spec.es6
Normal file
|
@ -0,0 +1,255 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import ModelQuery from '../../src/flux/models/query';
|
||||
import Attributes from '../../src/flux/attributes';
|
||||
import Message from '../../src/flux/models/message';
|
||||
import Thread from '../../src/flux/models/thread';
|
||||
import Account from '../../src/flux/models/account';
|
||||
|
||||
describe("ModelQuery", function ModelQuerySpecs() {
|
||||
beforeEach(() => {
|
||||
this.db = {};
|
||||
});
|
||||
|
||||
describe("where", () => {
|
||||
beforeEach(() => {
|
||||
this.q = new ModelQuery(Thread, this.db);
|
||||
this.m1 = Thread.attributes.id.equal(4);
|
||||
this.m2 = Thread.attributes.categories.contains('category-id');
|
||||
});
|
||||
|
||||
it("should accept an array of Matcher objects", () => {
|
||||
this.q.where([this.m1, this.m2]);
|
||||
expect(this.q._matchers.length).toBe(2);
|
||||
expect(this.q._matchers[0]).toBe(this.m1);
|
||||
expect(this.q._matchers[1]).toBe(this.m2);
|
||||
});
|
||||
|
||||
it("should accept a single Matcher object", () => {
|
||||
this.q.where(this.m1);
|
||||
expect(this.q._matchers.length).toBe(1);
|
||||
expect(this.q._matchers[0]).toBe(this.m1);
|
||||
});
|
||||
|
||||
it("should append to any existing where clauses", () => {
|
||||
this.q.where(this.m1);
|
||||
this.q.where(this.m2);
|
||||
expect(this.q._matchers.length).toBe(2);
|
||||
expect(this.q._matchers[0]).toBe(this.m1);
|
||||
expect(this.q._matchers[1]).toBe(this.m2);
|
||||
});
|
||||
|
||||
it("should accept a shorthand format", () => {
|
||||
this.q.where({id: 4, lastMessageReceivedTimestamp: 1234});
|
||||
expect(this.q._matchers.length).toBe(2);
|
||||
expect(this.q._matchers[0].attr.modelKey).toBe('id');
|
||||
expect(this.q._matchers[0].comparator).toBe('=');
|
||||
expect(this.q._matchers[0].val).toBe(4);
|
||||
});
|
||||
|
||||
it("should return the query so it can be chained", () => {
|
||||
expect(this.q.where({id: 4})).toBe(this.q);
|
||||
});
|
||||
|
||||
it("should immediately raise an exception if an un-queryable attribute is specified", () =>
|
||||
expect(() => {
|
||||
this.q.where({snippet: 'My Snippet'});
|
||||
}).toThrow()
|
||||
);
|
||||
|
||||
it("should immediately raise an exception if a non-existent attribute is specified", () =>
|
||||
expect(() => {
|
||||
this.q.where({looksLikeADuck: 'of course'});
|
||||
}).toThrow()
|
||||
);
|
||||
});
|
||||
|
||||
describe("order", () => {
|
||||
beforeEach(() => {
|
||||
this.q = new ModelQuery(Thread, this.db);
|
||||
this.o1 = Thread.attributes.lastMessageReceivedTimestamp.descending();
|
||||
this.o2 = Thread.attributes.subject.descending();
|
||||
});
|
||||
|
||||
it("should accept an array of SortOrders", () => {
|
||||
this.q.order([this.o1, this.o2]);
|
||||
expect(this.q._orders.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should accept a single SortOrder object", () => {
|
||||
this.q.order(this.o2);
|
||||
expect(this.q._orders.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should extend any existing ordering", () => {
|
||||
this.q.order(this.o1);
|
||||
this.q.order(this.o2);
|
||||
expect(this.q._orders.length).toBe(2);
|
||||
expect(this.q._orders[0]).toBe(this.o1);
|
||||
expect(this.q._orders[1]).toBe(this.o2);
|
||||
});
|
||||
|
||||
it("should return the query so it can be chained", () => {
|
||||
expect(this.q.order(this.o2)).toBe(this.q);
|
||||
});
|
||||
});
|
||||
|
||||
describe("include", () => {
|
||||
beforeEach(() => {
|
||||
this.q = new ModelQuery(Message, this.db);
|
||||
});
|
||||
|
||||
it("should throw an exception if the attribute is not a joined data attribute", () =>
|
||||
expect(() => {
|
||||
this.q.include(Message.attributes.unread);
|
||||
}).toThrow()
|
||||
|
||||
);
|
||||
|
||||
it("should add the provided property to the list of joined properties", () => {
|
||||
expect(this.q._includeJoinedData).toEqual([]);
|
||||
this.q.include(Message.attributes.body);
|
||||
expect(this.q._includeJoinedData).toEqual([Message.attributes.body]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("includeAll", () => {
|
||||
beforeEach(() => {
|
||||
this.q = new ModelQuery(Message, this.db);
|
||||
});
|
||||
|
||||
it("should add all the JoinedData attributes of the class", () => {
|
||||
expect(this.q._includeJoinedData).toEqual([]);
|
||||
this.q.includeAll();
|
||||
expect(this.q._includeJoinedData).toEqual([Message.attributes.body]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("response formatting", () =>
|
||||
it("should always return a Number for counts", () => {
|
||||
const q = new ModelQuery(Message, this.db);
|
||||
q.where({accountId: 'abcd'}).count();
|
||||
|
||||
const raw = [{count: "12"}];
|
||||
expect(q.formatResult(q.inflateResult(raw))).toBe(12);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("sql", () => {
|
||||
beforeEach(() => {
|
||||
this.runScenario = (klass, scenario) => {
|
||||
const q = new ModelQuery(klass, this.db);
|
||||
Attributes.Matcher.muid = 1;
|
||||
scenario.builder(q);
|
||||
expect(q.sql().trim()).toBe(scenario.sql.trim());
|
||||
};
|
||||
});
|
||||
|
||||
it("should finalize the query so no further changes can be made", () => {
|
||||
const q = new ModelQuery(Account, this.db);
|
||||
spyOn(q, 'finalize');
|
||||
q.sql();
|
||||
expect(q.finalize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should correctly generate queries with multiple where clauses", () => {
|
||||
this.runScenario(Account, {
|
||||
builder: (q) =>
|
||||
q.where({emailAddress: 'ben@nylas.com'}).where({id: 2}),
|
||||
sql: "SELECT `Account`.`data` FROM `Account` " +
|
||||
"WHERE `Account`.`email_address` = 'ben@nylas.com' AND `Account`.`id` = 2",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly escape single quotes with more double single quotes (LIKE)", () => {
|
||||
this.runScenario(Account, {
|
||||
builder: (q) =>
|
||||
q.where(Account.attributes.emailAddress.like("you're")),
|
||||
sql: "SELECT `Account`.`data` FROM `Account` WHERE `Account`.`email_address` like '%you''re%'",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly escape single quotes with more double single quotes (equal)", () => {
|
||||
this.runScenario(Account, {
|
||||
builder: (q) =>
|
||||
q.where(Account.attributes.emailAddress.equal("you're")),
|
||||
sql: "SELECT `Account`.`data` FROM `Account` WHERE `Account`.`email_address` = 'you''re'",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate COUNT queries", () => {
|
||||
this.runScenario(Thread, {
|
||||
builder: (q) =>
|
||||
q.where({accountId: 'abcd'}).count(),
|
||||
sql: "SELECT COUNT(*) as count FROM `Thread` " +
|
||||
"WHERE `Thread`.`account_id` = 'abcd' ",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate LIMIT 1 queries for single items", () => {
|
||||
this.runScenario(Thread, {
|
||||
builder: (q) =>
|
||||
q.where({accountId: 'abcd'}).one(),
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` " +
|
||||
"WHERE `Thread`.`account_id` = 'abcd' " +
|
||||
"ORDER BY `Thread`.`last_message_received_timestamp` DESC LIMIT 1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate `contains` queries using JOINS", () => {
|
||||
this.runScenario(Thread, {
|
||||
builder: (q) =>
|
||||
q.where(Thread.attributes.categories.contains('category-id')).where({id: '1234'}),
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` " +
|
||||
"INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` " +
|
||||
"WHERE `M1`.`value` = 'category-id' AND `Thread`.`id` = '1234' " +
|
||||
"ORDER BY `Thread`.`last_message_received_timestamp` DESC",
|
||||
});
|
||||
|
||||
this.runScenario(Thread, {
|
||||
builder: (q) =>
|
||||
q.where([Thread.attributes.categories.contains('l-1'), Thread.attributes.categories.contains('l-2')]),
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` " +
|
||||
"INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` " +
|
||||
"INNER JOIN `ThreadCategory` AS `M2` ON `M2`.`id` = `Thread`.`id` " +
|
||||
"WHERE `M1`.`value` = 'l-1' AND `M2`.`value` = 'l-2' " +
|
||||
"ORDER BY `Thread`.`last_message_received_timestamp` DESC",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate queries with the class's naturalSortOrder when one is available and no other orders are provided", () => {
|
||||
this.runScenario(Thread, {
|
||||
builder: (q) =>
|
||||
q.where({accountId: 'abcd'}),
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` " +
|
||||
"WHERE `Thread`.`account_id` = 'abcd' " +
|
||||
"ORDER BY `Thread`.`last_message_received_timestamp` DESC",
|
||||
});
|
||||
|
||||
this.runScenario(Thread, {
|
||||
builder: (q) =>
|
||||
q.where({accountId: 'abcd'}).order(Thread.attributes.lastMessageReceivedTimestamp.ascending()),
|
||||
sql: "SELECT `Thread`.`data` FROM `Thread` " +
|
||||
"WHERE `Thread`.`account_id` = 'abcd' " +
|
||||
"ORDER BY `Thread`.`last_message_received_timestamp` ASC",
|
||||
});
|
||||
|
||||
this.runScenario(Account, {
|
||||
builder: (q) =>
|
||||
q.where({id: 'abcd'}),
|
||||
sql: "SELECT `Account`.`data` FROM `Account` " +
|
||||
"WHERE `Account`.`id` = 'abcd' ",
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate queries requesting joined data attributes", () => {
|
||||
this.runScenario(Message, {
|
||||
builder: (q) =>
|
||||
q.where({id: '1234'}).include(Message.attributes.body),
|
||||
sql: "SELECT `Message`.`data`, IFNULL(`MessageBody`.`value`, '!NULLVALUE!') AS `body` " +
|
||||
"FROM `Message` LEFT OUTER JOIN `MessageBody` ON `MessageBody`.`id` = `Message`.`id` " +
|
||||
"WHERE `Message`.`id` = '1234' ORDER BY `Message`.`date` ASC",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
QuerySubscriptionPool = require('../../src/flux/models/query-subscription-pool').default
|
||||
DatabaseStore = require('../../src/flux/stores/database-store').default
|
||||
Label = require '../../src/flux/models/label'
|
||||
|
||||
describe "QuerySubscriptionPool", ->
|
||||
beforeEach ->
|
||||
@query = DatabaseStore.findAll(Label)
|
||||
@queryKey = @query.sql()
|
||||
QuerySubscriptionPool._subscriptions = {}
|
||||
QuerySubscriptionPool._cleanupChecks = []
|
||||
|
||||
describe "add", ->
|
||||
it "should add a new subscription with the callback", ->
|
||||
callback = jasmine.createSpy('callback')
|
||||
QuerySubscriptionPool.add(@query, callback)
|
||||
expect(QuerySubscriptionPool._subscriptions[@queryKey]).toBeDefined()
|
||||
|
||||
subscription = QuerySubscriptionPool._subscriptions[@queryKey]
|
||||
expect(subscription.hasCallback(callback)).toBe(true)
|
||||
|
||||
it "should yield database changes to the subscription", ->
|
||||
callback = jasmine.createSpy('callback')
|
||||
QuerySubscriptionPool.add(@query, callback)
|
||||
subscription = QuerySubscriptionPool._subscriptions[@queryKey]
|
||||
spyOn(subscription, 'applyChangeRecord')
|
||||
|
||||
record = {objectType: 'whateves'}
|
||||
QuerySubscriptionPool._onChange(record)
|
||||
expect(subscription.applyChangeRecord).toHaveBeenCalledWith(record)
|
||||
|
||||
describe "unsubscribe", ->
|
||||
it "should return an unsubscribe method", ->
|
||||
expect(QuerySubscriptionPool.add(@query, -> ) instanceof Function).toBe(true)
|
||||
|
||||
it "should remove the callback from the subscription", ->
|
||||
cb = ->
|
||||
|
||||
unsub = QuerySubscriptionPool.add(@query, cb)
|
||||
subscription = QuerySubscriptionPool._subscriptions[@queryKey]
|
||||
|
||||
expect(subscription.hasCallback(cb)).toBe(true)
|
||||
unsub()
|
||||
expect(subscription.hasCallback(cb)).toBe(false)
|
||||
|
||||
it "should wait before removing th subscription to make sure it's not reused", ->
|
||||
unsub = QuerySubscriptionPool.add(@query, -> )
|
||||
expect(QuerySubscriptionPool._subscriptions[@queryKey]).toBeDefined()
|
||||
unsub()
|
||||
expect(QuerySubscriptionPool._subscriptions[@queryKey]).toBeDefined()
|
||||
advanceClock()
|
||||
expect(QuerySubscriptionPool._subscriptions[@queryKey]).toBeUndefined()
|
60
spec/models/query-subscription-pool-spec.es6
Normal file
60
spec/models/query-subscription-pool-spec.es6
Normal file
|
@ -0,0 +1,60 @@
|
|||
import QuerySubscriptionPool from '../../src/flux/models/query-subscription-pool';
|
||||
import DatabaseStore from '../../src/flux/stores/database-store';
|
||||
import Label from '../../src/flux/models/label';
|
||||
|
||||
describe("QuerySubscriptionPool", function QuerySubscriptionPoolSpecs() {
|
||||
beforeEach(() => {
|
||||
this.query = DatabaseStore.findAll(Label);
|
||||
this.queryKey = this.query.sql();
|
||||
QuerySubscriptionPool._subscriptions = {};
|
||||
QuerySubscriptionPool._cleanupChecks = [];
|
||||
});
|
||||
|
||||
describe("add", () => {
|
||||
it("should add a new subscription with the callback", () => {
|
||||
const callback = jasmine.createSpy('callback');
|
||||
QuerySubscriptionPool.add(this.query, callback);
|
||||
expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeDefined();
|
||||
|
||||
const subscription = QuerySubscriptionPool._subscriptions[this.queryKey];
|
||||
expect(subscription.hasCallback(callback)).toBe(true);
|
||||
});
|
||||
|
||||
it("should yield database changes to the subscription", () => {
|
||||
const callback = jasmine.createSpy('callback');
|
||||
QuerySubscriptionPool.add(this.query, callback);
|
||||
const subscription = QuerySubscriptionPool._subscriptions[this.queryKey];
|
||||
spyOn(subscription, 'applyChangeRecord');
|
||||
|
||||
const record = {objectType: 'whateves'};
|
||||
QuerySubscriptionPool._onChange(record);
|
||||
expect(subscription.applyChangeRecord).toHaveBeenCalledWith(record);
|
||||
});
|
||||
|
||||
describe("unsubscribe", () => {
|
||||
it("should return an unsubscribe method", () => {
|
||||
expect(QuerySubscriptionPool.add(this.query, () => {}) instanceof Function).toBe(true);
|
||||
});
|
||||
|
||||
it("should remove the callback from the subscription", () => {
|
||||
const cb = () => {};
|
||||
|
||||
const unsub = QuerySubscriptionPool.add(this.query, cb);
|
||||
const subscription = QuerySubscriptionPool._subscriptions[this.queryKey];
|
||||
|
||||
expect(subscription.hasCallback(cb)).toBe(true);
|
||||
unsub();
|
||||
expect(subscription.hasCallback(cb)).toBe(false);
|
||||
});
|
||||
|
||||
it("should wait before removing th subscription to make sure it's not reused", () => {
|
||||
const unsub = QuerySubscriptionPool.add(this.query, () => {});
|
||||
expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeDefined();
|
||||
unsub();
|
||||
expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeDefined();
|
||||
advanceClock();
|
||||
expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,265 +0,0 @@
|
|||
DatabaseStore = require('../../src/flux/stores/database-store').default
|
||||
|
||||
QueryRange = require('../../src/flux/models/query-range').default
|
||||
MutableQueryResultSet = require('../../src/flux/models/mutable-query-result-set').default
|
||||
QuerySubscription = require('../../src/flux/models/query-subscription').default
|
||||
Thread = require('../../src/flux/models/thread').default
|
||||
Label = require '../../src/flux/models/label'
|
||||
Utils = require '../../src/flux/models/utils'
|
||||
|
||||
describe "QuerySubscription", ->
|
||||
describe "constructor", ->
|
||||
describe "when a query is provided", ->
|
||||
it "should finalize the query", ->
|
||||
query = DatabaseStore.findAll(Thread)
|
||||
subscription = new QuerySubscription(query)
|
||||
expect(query._finalized).toBe(true)
|
||||
|
||||
it "should throw an exception if the query is a count query, which cannot be observed", ->
|
||||
query = DatabaseStore.count(Thread)
|
||||
expect =>
|
||||
subscription = new QuerySubscription(query)
|
||||
.toThrow()
|
||||
|
||||
it "should call `update` to initialize the result set", ->
|
||||
query = DatabaseStore.findAll(Thread)
|
||||
spyOn(QuerySubscription.prototype, 'update')
|
||||
subscription = new QuerySubscription(query)
|
||||
expect(QuerySubscription.prototype.update).toHaveBeenCalled()
|
||||
|
||||
describe "when initialModels are provided", ->
|
||||
it "should apply the models and trigger", ->
|
||||
query = DatabaseStore.findAll(Thread)
|
||||
threads = [1..5].map (i) -> new Thread(id: i)
|
||||
subscription = new QuerySubscription(query, {initialModels: threads})
|
||||
expect(subscription._set).not.toBe(null)
|
||||
|
||||
describe "query", ->
|
||||
it "should return the query", ->
|
||||
query = DatabaseStore.findAll(Thread)
|
||||
subscription = new QuerySubscription(query)
|
||||
expect(subscription.query()).toBe(query)
|
||||
|
||||
describe "addCallback", ->
|
||||
it "should emit the last result to the new callback if one is available", ->
|
||||
cb = jasmine.createSpy('callback')
|
||||
runs =>
|
||||
subscription = new QuerySubscription(DatabaseStore.findAll(Thread))
|
||||
subscription._lastResult = 'something'
|
||||
subscription.addCallback(cb)
|
||||
waitsFor =>
|
||||
cb.callCount > 0
|
||||
expect =>
|
||||
expect(cb).toHaveBeenCalledWith('something')
|
||||
|
||||
describe "applyChangeRecord", ->
|
||||
spyOn(Utils, 'generateTempId').andCallFake => undefined
|
||||
|
||||
scenarios = [{
|
||||
name: "query with full set of objects (4)"
|
||||
query: DatabaseStore.findAll(Thread)
|
||||
.where(Thread.attributes.accountId.equal('a'))
|
||||
.limit(4)
|
||||
.offset(2)
|
||||
lastModels: [
|
||||
new Thread(accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 4)
|
||||
new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3),
|
||||
new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2),
|
||||
new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1),
|
||||
]
|
||||
tests: [{
|
||||
name: 'Item in set saved - new serverId, same sort value'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'a', serverId: 's-4', clientId: '4', lastMessageReceivedTimestamp: 4, subject: 'hello')]
|
||||
type: 'persist'
|
||||
nextModels:[
|
||||
new Thread(accountId: 'a', serverId: 's-4', clientId: '4', lastMessageReceivedTimestamp: 4, subject: 'hello')
|
||||
new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3),
|
||||
new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2),
|
||||
new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1),
|
||||
]
|
||||
mustUpdate: false
|
||||
mustTrigger: true
|
||||
},{
|
||||
name: 'Item in set saved - new sort value'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: 3.5)]
|
||||
type: 'persist'
|
||||
nextModels:[
|
||||
new Thread(accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 4),
|
||||
new Thread(accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: 3.5),
|
||||
new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3),
|
||||
new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2),
|
||||
]
|
||||
mustUpdate: true
|
||||
mustTrigger: true
|
||||
},{
|
||||
name: 'Item saved - does not match query clauses, offset > 0'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'b', clientId: '5', lastMessageReceivedTimestamp: 5)]
|
||||
type: 'persist'
|
||||
nextModels: 'unchanged'
|
||||
mustUpdate: true
|
||||
},{
|
||||
name: 'Item saved - matches query clauses'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: -2)]
|
||||
type: 'persist'
|
||||
mustUpdate: true
|
||||
},{
|
||||
name: 'Item in set saved - no longer matches query clauses'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'b', clientId: '4', lastMessageReceivedTimestamp: 4)]
|
||||
type: 'persist'
|
||||
nextModels: [
|
||||
new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3),
|
||||
new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2),
|
||||
new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1),
|
||||
]
|
||||
mustUpdate: true
|
||||
},{
|
||||
name: 'Item in set deleted'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'a', clientId: '4')]
|
||||
type: 'unpersist'
|
||||
nextModels: [
|
||||
new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3),
|
||||
new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2),
|
||||
new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1),
|
||||
]
|
||||
mustUpdate: true
|
||||
},{
|
||||
name: 'Item not in set deleted'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'a', clientId: '5')]
|
||||
type: 'unpersist'
|
||||
nextModels: 'unchanged'
|
||||
mustUpdate: false
|
||||
}]
|
||||
|
||||
},{
|
||||
name: "query with multiple sort orders"
|
||||
query: DatabaseStore.findAll(Thread)
|
||||
.where(Thread.attributes.accountId.equal('a'))
|
||||
.limit(4)
|
||||
.offset(2)
|
||||
.order([
|
||||
Thread.attributes.lastMessageReceivedTimestamp.ascending(),
|
||||
Thread.attributes.unread.descending()
|
||||
])
|
||||
lastModels: [
|
||||
new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1, unread: true)
|
||||
new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 1, unread: false)
|
||||
new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: false)
|
||||
new Thread(accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 2, unread: true)
|
||||
]
|
||||
tests: [{
|
||||
name: 'Item in set saved, secondary sort order changed'
|
||||
change:
|
||||
objectClass: Thread.name
|
||||
objects: [new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: true)]
|
||||
type: 'persist'
|
||||
mustUpdate: true
|
||||
}]
|
||||
}]
|
||||
|
||||
jasmine.unspy(Utils, 'generateTempId')
|
||||
|
||||
describe "scenarios", ->
|
||||
scenarios.forEach (scenario) =>
|
||||
scenario.tests.forEach (test) =>
|
||||
it "with #{scenario.name}, should correctly apply #{test.name}", ->
|
||||
subscription = new QuerySubscription(scenario.query)
|
||||
subscription._set = new MutableQueryResultSet()
|
||||
subscription._set.addModelsInRange(scenario.lastModels, new QueryRange(start: 0, end: scenario.lastModels.length))
|
||||
|
||||
spyOn(subscription, 'update')
|
||||
spyOn(subscription, '_createResultAndTrigger')
|
||||
subscription._updateInFlight = false
|
||||
subscription.applyChangeRecord(test.change)
|
||||
|
||||
if test.mustUpdate
|
||||
expect(subscription.update).toHaveBeenCalledWith({mustRefetchEntireRange: true})
|
||||
else
|
||||
if test.nextModels is 'unchanged'
|
||||
expect(subscription._set.models()).toEqual(scenario.lastModels)
|
||||
else
|
||||
expect(subscription._set.models()).toEqual(test.nextModels)
|
||||
|
||||
if test.mustTriger
|
||||
expect(subscription._createResultAndTrigger).toHaveBeenCalled()
|
||||
|
||||
describe "update", ->
|
||||
beforeEach ->
|
||||
spyOn(QuerySubscription.prototype, '_fetchRange').andCallFake ->
|
||||
@_set ?= new MutableQueryResultSet()
|
||||
Promise.resolve()
|
||||
|
||||
describe "when the query has an infinite range", ->
|
||||
it "should call _fetchRange for the entire range", ->
|
||||
subscription = new QuerySubscription(DatabaseStore.findAll(Thread))
|
||||
subscription.update()
|
||||
advanceClock()
|
||||
expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: true, version: 1})
|
||||
|
||||
it "should fetch full full models only when the previous set is empty", ->
|
||||
subscription = new QuerySubscription(DatabaseStore.findAll(Thread))
|
||||
subscription._set = new MutableQueryResultSet()
|
||||
subscription._set.addModelsInRange([new Thread()], new QueryRange(start: 0, end: 1))
|
||||
subscription.update()
|
||||
advanceClock()
|
||||
expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: false, version: 1})
|
||||
|
||||
describe "when the query has a range", ->
|
||||
beforeEach ->
|
||||
@query = DatabaseStore.findAll(Thread).limit(10)
|
||||
|
||||
describe "when we have no current range", ->
|
||||
it "should call _fetchRange for the entire range and fetch full models", ->
|
||||
subscription = new QuerySubscription(@query)
|
||||
subscription._set = null
|
||||
subscription.update()
|
||||
advanceClock()
|
||||
expect(subscription._fetchRange).toHaveBeenCalledWith(@query.range(), {fetchEntireModels: true, version: 1})
|
||||
|
||||
describe "when we have a previous range", ->
|
||||
|
||||
it "should call _fetchRange with the missingRange", ->
|
||||
customRange = jasmine.createSpy('customRange1')
|
||||
spyOn(QueryRange, 'rangesBySubtracting').andReturn [customRange]
|
||||
subscription = new QuerySubscription(@query)
|
||||
subscription._set = new MutableQueryResultSet()
|
||||
subscription._set.addModelsInRange([new Thread()], new QueryRange(start: 0, end: 1))
|
||||
|
||||
advanceClock()
|
||||
subscription._fetchRange.reset()
|
||||
subscription._updateInFlight = false
|
||||
subscription.update()
|
||||
advanceClock()
|
||||
expect(subscription._fetchRange.callCount).toBe(1)
|
||||
expect(subscription._fetchRange.calls[0].args).toEqual([customRange, {fetchEntireModels: true, version: 1}])
|
||||
|
||||
it "should call _fetchRange for the entire query range when the missing range encompasses more than one range", ->
|
||||
customRange1 = jasmine.createSpy('customRange1')
|
||||
customRange2 = jasmine.createSpy('customRange2')
|
||||
spyOn(QueryRange, 'rangesBySubtracting').andReturn [customRange1, customRange2]
|
||||
|
||||
range = new QueryRange(start: 0, end: 1)
|
||||
subscription = new QuerySubscription(@query)
|
||||
subscription._set = new MutableQueryResultSet()
|
||||
subscription._set.addModelsInRange([new Thread()], range)
|
||||
|
||||
advanceClock()
|
||||
subscription._fetchRange.reset()
|
||||
subscription._updateInFlight = false
|
||||
subscription.update()
|
||||
advanceClock()
|
||||
expect(subscription._fetchRange.callCount).toBe(1)
|
||||
expect(subscription._fetchRange.calls[0].args).toEqual([@query.range(), {fetchEntireModels: true, version: 1}])
|
302
spec/models/query-subscription-spec.es6
Normal file
302
spec/models/query-subscription-spec.es6
Normal file
|
@ -0,0 +1,302 @@
|
|||
import DatabaseStore from '../../src/flux/stores/database-store';
|
||||
|
||||
import QueryRange from '../../src/flux/models/query-range';
|
||||
import MutableQueryResultSet from '../../src/flux/models/mutable-query-result-set';
|
||||
import QuerySubscription from '../../src/flux/models/query-subscription';
|
||||
import Thread from '../../src/flux/models/thread';
|
||||
import Utils from '../../src/flux/models/utils';
|
||||
|
||||
describe("QuerySubscription", function QuerySubscriptionSpecs() {
|
||||
describe("constructor", () =>
|
||||
describe("when a query is provided", () => {
|
||||
it("should finalize the query", () => {
|
||||
const query = DatabaseStore.findAll(Thread);
|
||||
const subscription = new QuerySubscription(query);
|
||||
expect(subscription).toBeDefined();
|
||||
expect(query._finalized).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw an exception if the query is a count query, which cannot be observed", () => {
|
||||
const query = DatabaseStore.count(Thread);
|
||||
expect(() => {
|
||||
const subscription = new QuerySubscription(query);
|
||||
return subscription;
|
||||
})
|
||||
.toThrow();
|
||||
});
|
||||
|
||||
it("should call `update` to initialize the result set", () => {
|
||||
const query = DatabaseStore.findAll(Thread);
|
||||
spyOn(QuerySubscription.prototype, 'update');
|
||||
const subscription = new QuerySubscription(query);
|
||||
expect(subscription).toBeDefined();
|
||||
expect(QuerySubscription.prototype.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when initialModels are provided", () =>
|
||||
it("should apply the models and trigger", () => {
|
||||
const query = DatabaseStore.findAll(Thread);
|
||||
const threads = [1, 2, 3, 4, 5].map(i => new Thread({id: i}));
|
||||
const subscription = new QuerySubscription(query, {initialModels: threads});
|
||||
expect(subscription._set).not.toBe(null);
|
||||
})
|
||||
|
||||
);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("query", () =>
|
||||
it("should return the query", () => {
|
||||
const query = DatabaseStore.findAll(Thread);
|
||||
const subscription = new QuerySubscription(query);
|
||||
expect(subscription.query()).toBe(query);
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("addCallback", () =>
|
||||
it("should emit the last result to the new callback if one is available", () => {
|
||||
const cb = jasmine.createSpy('callback');
|
||||
spyOn(QuerySubscription.prototype, 'update').andReturn();
|
||||
const subscription = new QuerySubscription(DatabaseStore.findAll(Thread));
|
||||
subscription._lastResult = 'something';
|
||||
runs(() => subscription.addCallback(cb));
|
||||
waitsFor(() => cb.callCount > 0);
|
||||
runs(() => expect(cb).toHaveBeenCalledWith('something'));
|
||||
})
|
||||
);
|
||||
|
||||
describe("applyChangeRecord", () => {
|
||||
spyOn(Utils, 'generateTempId').andCallFake(() => undefined);
|
||||
|
||||
const scenarios = [{
|
||||
name: "query with full set of objects (4)",
|
||||
query: DatabaseStore.findAll(Thread).where(Thread.attributes.accountId.equal('a')).limit(4).offset(2),
|
||||
lastModels: [
|
||||
new Thread({accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 4}),
|
||||
new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),
|
||||
new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),
|
||||
new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),
|
||||
],
|
||||
tests: [{
|
||||
name: 'Item in set saved - new serverId, same sort value',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'a', serverId: 's-4', clientId: '4', lastMessageReceivedTimestamp: 4, subject: 'hello'})],
|
||||
type: 'persist',
|
||||
},
|
||||
nextModels: [
|
||||
new Thread({accountId: 'a', serverId: 's-4', clientId: '4', lastMessageReceivedTimestamp: 4, subject: 'hello'}),
|
||||
new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),
|
||||
new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),
|
||||
new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),
|
||||
],
|
||||
mustUpdate: false,
|
||||
mustTrigger: true,
|
||||
}, {
|
||||
name: 'Item in set saved - new sort value',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: 3.5})],
|
||||
type: 'persist',
|
||||
},
|
||||
nextModels: [
|
||||
new Thread({accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 4}),
|
||||
new Thread({accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: 3.5}),
|
||||
new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),
|
||||
new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),
|
||||
],
|
||||
mustUpdate: true,
|
||||
mustTrigger: true,
|
||||
}, {
|
||||
name: 'Item saved - does not match query clauses, offset > 0',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'b', clientId: '5', lastMessageReceivedTimestamp: 5})],
|
||||
type: 'persist',
|
||||
},
|
||||
nextModels: 'unchanged',
|
||||
mustUpdate: true,
|
||||
}, {
|
||||
name: 'Item saved - matches query clauses',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: -2})],
|
||||
type: 'persist',
|
||||
},
|
||||
mustUpdate: true,
|
||||
}, {
|
||||
name: 'Item in set saved - no longer matches query clauses',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'b', clientId: '4', lastMessageReceivedTimestamp: 4})],
|
||||
type: 'persist',
|
||||
},
|
||||
nextModels: [
|
||||
new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),
|
||||
new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),
|
||||
new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),
|
||||
],
|
||||
mustUpdate: true,
|
||||
}, {
|
||||
name: 'Item in set deleted',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'a', clientId: '4'})],
|
||||
type: 'unpersist',
|
||||
},
|
||||
nextModels: [
|
||||
new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),
|
||||
new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),
|
||||
new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),
|
||||
],
|
||||
mustUpdate: true,
|
||||
}, {
|
||||
name: 'Item not in set deleted',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'a', clientId: '5'})],
|
||||
type: 'unpersist',
|
||||
},
|
||||
nextModels: 'unchanged',
|
||||
mustUpdate: false,
|
||||
}],
|
||||
}, {
|
||||
name: "query with multiple sort orders",
|
||||
query: DatabaseStore.findAll(Thread).where(Thread.attributes.accountId.equal('a')).limit(4).offset(2).order([
|
||||
Thread.attributes.lastMessageReceivedTimestamp.ascending(),
|
||||
Thread.attributes.unread.descending(),
|
||||
]),
|
||||
lastModels: [
|
||||
new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1, unread: true}),
|
||||
new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 1, unread: false}),
|
||||
new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: false}),
|
||||
new Thread({accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 2, unread: true}),
|
||||
],
|
||||
tests: [{
|
||||
name: 'Item in set saved, secondary sort order changed',
|
||||
change: {
|
||||
objectClass: Thread.name,
|
||||
objects: [new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: true})],
|
||||
type: 'persist',
|
||||
},
|
||||
mustUpdate: true,
|
||||
}],
|
||||
}];
|
||||
|
||||
jasmine.unspy(Utils, 'generateTempId');
|
||||
|
||||
describe("scenarios", () =>
|
||||
scenarios.forEach(scenario => {
|
||||
scenario.tests.forEach(test => {
|
||||
it(`with ${scenario.name}, should correctly apply ${test.name}`, () => {
|
||||
const subscription = new QuerySubscription(scenario.query);
|
||||
subscription._set = new MutableQueryResultSet();
|
||||
subscription._set.addModelsInRange(scenario.lastModels, new QueryRange({start: 0, end: scenario.lastModels.length}));
|
||||
|
||||
spyOn(subscription, 'update');
|
||||
spyOn(subscription, '_createResultAndTrigger');
|
||||
subscription._updateInFlight = false;
|
||||
subscription.applyChangeRecord(test.change);
|
||||
|
||||
if (test.mustUpdate) {
|
||||
expect(subscription.update).toHaveBeenCalledWith({mustRefetchEntireRange: true});
|
||||
} else {
|
||||
if (test.nextModels === 'unchanged') {
|
||||
expect(subscription._set.models()).toEqual(scenario.lastModels);
|
||||
} else {
|
||||
expect(subscription._set.models()).toEqual(test.nextModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (test.mustTriger) {
|
||||
expect(subscription._createResultAndTrigger).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
beforeEach(() =>
|
||||
spyOn(QuerySubscription.prototype, '_fetchRange').andCallFake(() => {
|
||||
if (this._set == null) { this._set = new MutableQueryResultSet(); }
|
||||
return Promise.resolve();
|
||||
})
|
||||
);
|
||||
|
||||
describe("when the query has an infinite range", () => {
|
||||
it("should call _fetchRange for the entire range", () => {
|
||||
const subscription = new QuerySubscription(DatabaseStore.findAll(Thread));
|
||||
subscription.update();
|
||||
advanceClock();
|
||||
expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: true, version: 1});
|
||||
});
|
||||
|
||||
it("should fetch full full models only when the previous set is empty", () => {
|
||||
const subscription = new QuerySubscription(DatabaseStore.findAll(Thread));
|
||||
subscription._set = new MutableQueryResultSet();
|
||||
subscription._set.addModelsInRange([new Thread()], new QueryRange({start: 0, end: 1}));
|
||||
subscription.update();
|
||||
advanceClock();
|
||||
expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: false, version: 1});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the query has a range", () => {
|
||||
beforeEach(() => {
|
||||
this.query = DatabaseStore.findAll(Thread).limit(10);
|
||||
});
|
||||
|
||||
describe("when we have no current range", () =>
|
||||
it("should call _fetchRange for the entire range and fetch full models", () => {
|
||||
const subscription = new QuerySubscription(this.query);
|
||||
subscription._set = null;
|
||||
subscription.update();
|
||||
advanceClock();
|
||||
expect(subscription._fetchRange).toHaveBeenCalledWith(this.query.range(), {fetchEntireModels: true, version: 1});
|
||||
})
|
||||
);
|
||||
|
||||
describe("when we have a previous range", () => {
|
||||
it("should call _fetchRange with the missingRange", () => {
|
||||
const customRange = jasmine.createSpy('customRange1');
|
||||
spyOn(QueryRange, 'rangesBySubtracting').andReturn([customRange]);
|
||||
const subscription = new QuerySubscription(this.query);
|
||||
subscription._set = new MutableQueryResultSet();
|
||||
subscription._set.addModelsInRange([new Thread()], new QueryRange({start: 0, end: 1}));
|
||||
|
||||
advanceClock();
|
||||
subscription._fetchRange.reset();
|
||||
subscription._updateInFlight = false;
|
||||
subscription.update();
|
||||
advanceClock();
|
||||
expect(subscription._fetchRange.callCount).toBe(1);
|
||||
expect(subscription._fetchRange.calls[0].args).toEqual([customRange, {fetchEntireModels: true, version: 1}]);
|
||||
});
|
||||
|
||||
it("should call _fetchRange for the entire query range when the missing range encompasses more than one range", () => {
|
||||
const customRange1 = jasmine.createSpy('customRange1');
|
||||
const customRange2 = jasmine.createSpy('customRange2');
|
||||
spyOn(QueryRange, 'rangesBySubtracting').andReturn([customRange1, customRange2]);
|
||||
|
||||
const range = new QueryRange({start: 0, end: 1});
|
||||
const subscription = new QuerySubscription(this.query);
|
||||
subscription._set = new MutableQueryResultSet();
|
||||
subscription._set.addModelsInRange([new Thread()], range);
|
||||
|
||||
advanceClock();
|
||||
subscription._fetchRange.reset();
|
||||
subscription._updateInFlight = false;
|
||||
subscription.update();
|
||||
advanceClock();
|
||||
expect(subscription._fetchRange.callCount).toBe(1);
|
||||
expect(subscription._fetchRange.calls[0].args).toEqual([this.query.range(), {fetchEntireModels: true, version: 1}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
TestModel = require '../fixtures/db-test-model'
|
||||
Attributes = require('../../src/flux/attributes').default
|
||||
DatabaseSetupQueryBuilder = require('../../src/flux/stores/database-setup-query-builder').default
|
||||
|
||||
describe "DatabaseSetupQueryBuilder", ->
|
||||
beforeEach ->
|
||||
@builder = new DatabaseSetupQueryBuilder()
|
||||
|
||||
describe "setupQueriesForTable", ->
|
||||
it "should return the queries for creating the table and the primary unique index", ->
|
||||
TestModel.attributes =
|
||||
'attrQueryable': Attributes.DateTime
|
||||
queryable: true
|
||||
modelKey: 'attrQueryable'
|
||||
jsonKey: 'attr_queryable'
|
||||
|
||||
'attrNonQueryable': Attributes.Collection
|
||||
modelKey: 'attrNonQueryable'
|
||||
jsonKey: 'attr_non_queryable'
|
||||
queries = @builder.setupQueriesForTable(TestModel)
|
||||
expected = [
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,attr_queryable INTEGER)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)'
|
||||
]
|
||||
for query,i in queries
|
||||
expect(query).toBe(expected[i])
|
||||
|
||||
it "should correctly create join tables for models that have queryable collections", ->
|
||||
TestModel.configureWithCollectionAttribute()
|
||||
queries = @builder.setupQueriesForTable(TestModel)
|
||||
expected = [
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,client_id TEXT,server_id TEXT,other TEXT)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',
|
||||
'CREATE TABLE IF NOT EXISTS `TestModelCategory` (id TEXT KEY,`value` TEXT,other TEXT)'
|
||||
'CREATE INDEX IF NOT EXISTS `TestModelCategory_id` ON `TestModelCategory` (`id` ASC)'
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModelCategory_val_id` ON `TestModelCategory` (`value` ASC, `id` ASC)',
|
||||
]
|
||||
for query,i in queries
|
||||
expect(query).toBe(expected[i])
|
||||
|
||||
it "should use the correct column type for each attribute", ->
|
||||
TestModel.configureWithAllAttributes()
|
||||
queries = @builder.setupQueriesForTable(TestModel)
|
||||
expect(queries[0]).toBe('CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,datetime INTEGER,string-json-key TEXT,boolean INTEGER,number INTEGER)')
|
||||
|
||||
describe "when the model provides additional sqlite config", ->
|
||||
it "the setup method should return these queries", ->
|
||||
TestModel.configureWithAdditionalSQLiteConfig()
|
||||
spyOn(TestModel.additionalSQLiteConfig, 'setup').andCallThrough()
|
||||
queries = @builder.setupQueriesForTable(TestModel)
|
||||
expect(TestModel.additionalSQLiteConfig.setup).toHaveBeenCalledWith()
|
||||
expect(queries.pop()).toBe('CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(last_message_received_timestamp DESC, account_id, id)')
|
||||
|
||||
it "should not fail if additional config is present, but setup is undefined", ->
|
||||
delete TestModel.additionalSQLiteConfig['setup']
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
|
||||
expect( => @builder.setupQueriesForTable(TestModel)).not.toThrow()
|
72
spec/stores/database-setup-query-builder-spec.es6
Normal file
72
spec/stores/database-setup-query-builder-spec.es6
Normal file
|
@ -0,0 +1,72 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import TestModel from '../fixtures/db-test-model';
|
||||
import Attributes from '../../src/flux/attributes';
|
||||
import DatabaseSetupQueryBuilder from '../../src/flux/stores/database-setup-query-builder';
|
||||
|
||||
describe("DatabaseSetupQueryBuilder", function DatabaseSetupQueryBuilderSpecs() {
|
||||
beforeEach(() => {
|
||||
this.builder = new DatabaseSetupQueryBuilder();
|
||||
});
|
||||
|
||||
describe("setupQueriesForTable", () => {
|
||||
it("should return the queries for creating the table and the primary unique index", () => {
|
||||
TestModel.attributes = {
|
||||
'attrQueryable': Attributes.DateTime({
|
||||
queryable: true,
|
||||
modelKey: 'attrQueryable',
|
||||
jsonKey: 'attr_queryable',
|
||||
}),
|
||||
|
||||
'attrNonQueryable': Attributes.Collection({
|
||||
modelKey: 'attrNonQueryable',
|
||||
jsonKey: 'attr_non_queryable',
|
||||
}),
|
||||
};
|
||||
const queries = this.builder.setupQueriesForTable(TestModel);
|
||||
const expected = [
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,attr_queryable INTEGER)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',
|
||||
];
|
||||
queries.map((query, i) =>
|
||||
expect(query).toBe(expected[i])
|
||||
);
|
||||
});
|
||||
|
||||
it("should correctly create join tables for models that have queryable collections", () => {
|
||||
TestModel.configureWithCollectionAttribute();
|
||||
const queries = this.builder.setupQueriesForTable(TestModel);
|
||||
const expected = [
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,client_id TEXT,server_id TEXT,other TEXT)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',
|
||||
'CREATE TABLE IF NOT EXISTS `TestModelCategory` (id TEXT KEY,`value` TEXT,other TEXT)',
|
||||
'CREATE INDEX IF NOT EXISTS `TestModelCategory_id` ON `TestModelCategory` (`id` ASC)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModelCategory_val_id` ON `TestModelCategory` (`value` ASC, `id` ASC)',
|
||||
];
|
||||
queries.map((query, i) =>
|
||||
expect(query).toBe(expected[i])
|
||||
);
|
||||
});
|
||||
|
||||
it("should use the correct column type for each attribute", () => {
|
||||
TestModel.configureWithAllAttributes();
|
||||
const queries = this.builder.setupQueriesForTable(TestModel);
|
||||
expect(queries[0]).toBe('CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,datetime INTEGER,string-json-key TEXT,boolean INTEGER,number INTEGER)');
|
||||
});
|
||||
|
||||
describe("when the model provides additional sqlite config", () => {
|
||||
it("the setup method should return these queries", () => {
|
||||
TestModel.configureWithAdditionalSQLiteConfig();
|
||||
spyOn(TestModel.additionalSQLiteConfig, 'setup').andCallThrough();
|
||||
const queries = this.builder.setupQueriesForTable(TestModel);
|
||||
expect(TestModel.additionalSQLiteConfig.setup).toHaveBeenCalledWith();
|
||||
expect(queries.pop()).toBe('CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(last_message_received_timestamp DESC, account_id, id)');
|
||||
});
|
||||
|
||||
it("should not fail if additional config is present, but setup is undefined", () => {
|
||||
delete TestModel.additionalSQLiteConfig.setup;
|
||||
this.m = new TestModel({id: 'local-6806434c-b0cd', body: 'hello world'});
|
||||
expect(() => this.builder.setupQueriesForTable(TestModel)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,239 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
Label = require '../../src/flux/models/label'
|
||||
Thread = require('../../src/flux/models/thread').default
|
||||
TestModel = require '../fixtures/db-test-model'
|
||||
ModelQuery = require('../../src/flux/models/query').default
|
||||
DatabaseStore = require('../../src/flux/stores/database-store').default
|
||||
|
||||
testMatchers = {'id': 'b'}
|
||||
testModelInstance = new TestModel(id: "1234")
|
||||
testModelInstanceA = new TestModel(id: "AAA")
|
||||
testModelInstanceB = new TestModel(id: "BBB")
|
||||
|
||||
describe "DatabaseStore", ->
|
||||
beforeEach ->
|
||||
TestModel.configureBasic()
|
||||
|
||||
DatabaseStore._atomicallyQueue = undefined
|
||||
DatabaseStore._mutationQueue = undefined
|
||||
DatabaseStore._inTransaction = false
|
||||
|
||||
spyOn(ModelQuery.prototype, 'where').andCallThrough()
|
||||
spyOn(DatabaseStore, 'accumulateAndTrigger').andCallFake -> Promise.resolve()
|
||||
|
||||
@performed = []
|
||||
|
||||
# Note: We spy on _query and test all of the convenience methods that sit above
|
||||
# it. None of these tests evaluate whether _query works!
|
||||
jasmine.unspy(DatabaseStore, "_query")
|
||||
spyOn(DatabaseStore, "_query").andCallFake (query, values=[], options={}) =>
|
||||
@performed.push({query: query, values: values})
|
||||
return Promise.resolve([])
|
||||
|
||||
describe "find", ->
|
||||
it "should return a ModelQuery for retrieving a single item by Id", ->
|
||||
q = DatabaseStore.find(TestModel, "4")
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = '4' LIMIT 1")
|
||||
|
||||
describe "findBy", ->
|
||||
it "should pass the provided predicates on to the ModelQuery", ->
|
||||
matchers = {'id': 'b'}
|
||||
DatabaseStore.findBy(TestModel, testMatchers)
|
||||
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers)
|
||||
|
||||
it "should return a ModelQuery ready to be executed", ->
|
||||
q = DatabaseStore.findBy(TestModel, testMatchers)
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' LIMIT 1")
|
||||
|
||||
describe "findAll", ->
|
||||
it "should pass the provided predicates on to the ModelQuery", ->
|
||||
DatabaseStore.findAll(TestModel, testMatchers)
|
||||
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers)
|
||||
|
||||
it "should return a ModelQuery ready to be executed", ->
|
||||
q = DatabaseStore.findAll(TestModel, testMatchers)
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' ")
|
||||
|
||||
describe "modelify", ->
|
||||
beforeEach ->
|
||||
@models = [
|
||||
new Thread(clientId: 'local-A'),
|
||||
new Thread(clientId: 'local-B'),
|
||||
new Thread(clientId: 'local-C'),
|
||||
new Thread(clientId: 'local-D', serverId: 'SERVER:D'),
|
||||
new Thread(clientId: 'local-E', serverId: 'SERVER:E'),
|
||||
new Thread(clientId: 'local-F', serverId: 'SERVER:F'),
|
||||
new Thread(clientId: 'local-G', serverId: 'SERVER:G')
|
||||
]
|
||||
# Actually returns correct sets for queries, since matchers can evaluate
|
||||
# themselves against models in memory
|
||||
spyOn(DatabaseStore, 'run').andCallFake (query) =>
|
||||
results = @models.filter((model) ->
|
||||
query._matchers.every((matcher) ->
|
||||
matcher.evaluate(model)
|
||||
)
|
||||
)
|
||||
Promise.resolve(results)
|
||||
|
||||
describe "when given an array or input that is not an array", ->
|
||||
it "resolves immediately with an empty array", ->
|
||||
waitsForPromise =>
|
||||
DatabaseStore.modelify(Thread, null).then (output) =>
|
||||
expect(output).toEqual([])
|
||||
|
||||
describe "when given an array of mixed IDs, clientIDs, and models", ->
|
||||
it "resolves with an array of models", ->
|
||||
input = ['SERVER:F', 'local-B', 'local-C', 'SERVER:D', @models[6]]
|
||||
expectedOutput = [@models[5], @models[1], @models[2], @models[3], @models[6]]
|
||||
waitsForPromise =>
|
||||
DatabaseStore.modelify(Thread, input).then (output) =>
|
||||
expect(output).toEqual(expectedOutput)
|
||||
|
||||
describe "when the input is only IDs", ->
|
||||
it "resolves with an array of models", ->
|
||||
input = ['SERVER:D', 'SERVER:F', 'SERVER:G']
|
||||
expectedOutput = [@models[3], @models[5], @models[6]]
|
||||
waitsForPromise =>
|
||||
DatabaseStore.modelify(Thread, input).then (output) =>
|
||||
expect(output).toEqual(expectedOutput)
|
||||
|
||||
describe "when the input is only clientIDs", ->
|
||||
it "resolves with an array of models", ->
|
||||
input = ['local-A', 'local-B', 'local-C', 'local-D']
|
||||
expectedOutput = [@models[0], @models[1], @models[2], @models[3]]
|
||||
waitsForPromise =>
|
||||
DatabaseStore.modelify(Thread, input).then (output) =>
|
||||
expect(output).toEqual(expectedOutput)
|
||||
|
||||
describe "when the input is all models", ->
|
||||
it "resolves with an array of models", ->
|
||||
input = [@models[0], @models[1], @models[2], @models[3]]
|
||||
expectedOutput = [@models[0], @models[1], @models[2], @models[3]]
|
||||
waitsForPromise =>
|
||||
DatabaseStore.modelify(Thread, input).then (output) =>
|
||||
expect(output).toEqual(expectedOutput)
|
||||
|
||||
describe "count", ->
|
||||
it "should pass the provided predicates on to the ModelQuery", ->
|
||||
DatabaseStore.findAll(TestModel, testMatchers)
|
||||
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers)
|
||||
|
||||
it "should return a ModelQuery configured for COUNT ready to be executed", ->
|
||||
q = DatabaseStore.findAll(TestModel, testMatchers)
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' ")
|
||||
|
||||
describe "inTransaction", ->
|
||||
it "calls the provided function inside an exclusive transaction", ->
|
||||
waitsForPromise =>
|
||||
DatabaseStore.inTransaction( =>
|
||||
DatabaseStore._query("TEST")
|
||||
).then =>
|
||||
expect(@performed.length).toBe 3
|
||||
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[1].query).toBe "TEST"
|
||||
expect(@performed[2].query).toBe "COMMIT"
|
||||
|
||||
it "preserves resolved values", ->
|
||||
waitsForPromise =>
|
||||
DatabaseStore.inTransaction( =>
|
||||
DatabaseStore._query("TEST")
|
||||
return Promise.resolve("myValue")
|
||||
).then (myValue) =>
|
||||
expect(myValue).toBe "myValue"
|
||||
|
||||
it "always fires a COMMIT, even if the body function fails", ->
|
||||
waitsForPromise =>
|
||||
DatabaseStore.inTransaction( =>
|
||||
throw new Error("BOOO")
|
||||
).catch =>
|
||||
expect(@performed.length).toBe 2
|
||||
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[1].query).toBe "COMMIT"
|
||||
|
||||
it "can be called multiple times and get queued", ->
|
||||
waitsForPromise =>
|
||||
Promise.all([
|
||||
DatabaseStore.inTransaction( -> )
|
||||
DatabaseStore.inTransaction( -> )
|
||||
DatabaseStore.inTransaction( -> )
|
||||
]).then =>
|
||||
expect(@performed.length).toBe 6
|
||||
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[1].query).toBe "COMMIT"
|
||||
expect(@performed[2].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[3].query).toBe "COMMIT"
|
||||
expect(@performed[4].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[5].query).toBe "COMMIT"
|
||||
|
||||
it "carries on if one of them fails, but still calls the COMMIT for the failed block", ->
|
||||
caughtError = false
|
||||
DatabaseStore.inTransaction( => DatabaseStore._query("ONE") )
|
||||
DatabaseStore.inTransaction( => throw new Error("fail") ).catch ->
|
||||
caughtError = true
|
||||
DatabaseStore.inTransaction( => DatabaseStore._query("THREE") )
|
||||
advanceClock(100)
|
||||
expect(@performed.length).toBe 8
|
||||
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[1].query).toBe "ONE"
|
||||
expect(@performed[2].query).toBe "COMMIT"
|
||||
expect(@performed[3].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[4].query).toBe "COMMIT"
|
||||
expect(@performed[5].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[6].query).toBe "THREE"
|
||||
expect(@performed[7].query).toBe "COMMIT"
|
||||
expect(caughtError).toBe true
|
||||
|
||||
it "is actually running in series and blocks on never-finishing specs", ->
|
||||
resolver = null
|
||||
DatabaseStore.inTransaction( -> )
|
||||
advanceClock(100)
|
||||
expect(@performed.length).toBe 2
|
||||
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[1].query).toBe "COMMIT"
|
||||
DatabaseStore.inTransaction( -> new Promise (resolve, reject) -> resolver = resolve)
|
||||
advanceClock(100)
|
||||
blockedPromiseDone = false
|
||||
DatabaseStore.inTransaction( -> ).then =>
|
||||
blockedPromiseDone = true
|
||||
advanceClock(100)
|
||||
expect(@performed.length).toBe 3
|
||||
expect(@performed[2].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(blockedPromiseDone).toBe false
|
||||
|
||||
# Now that we've made our assertion about blocking, we need to clean up
|
||||
# our test and actually resolve that blocked promise now, otherwise
|
||||
# remaining tests won't run properly.
|
||||
advanceClock(100)
|
||||
resolver()
|
||||
advanceClock(100)
|
||||
expect(blockedPromiseDone).toBe true
|
||||
advanceClock(100)
|
||||
|
||||
it "can be called multiple times and preserve return values", ->
|
||||
waitsForPromise =>
|
||||
v1 = null
|
||||
v2 = null
|
||||
v3 = null
|
||||
Promise.all([
|
||||
DatabaseStore.inTransaction( -> "a" ).then (val) -> v1 = val
|
||||
DatabaseStore.inTransaction( -> "b" ).then (val) -> v2 = val
|
||||
DatabaseStore.inTransaction( -> "c" ).then (val) -> v3 = val
|
||||
]).then =>
|
||||
expect(v1).toBe "a"
|
||||
expect(v2).toBe "b"
|
||||
expect(v3).toBe "c"
|
||||
|
||||
it "can be called multiple times and get queued", ->
|
||||
waitsForPromise =>
|
||||
DatabaseStore.inTransaction( -> )
|
||||
.then -> DatabaseStore.inTransaction( -> )
|
||||
.then -> DatabaseStore.inTransaction( -> )
|
||||
.then =>
|
||||
expect(@performed.length).toBe 6
|
||||
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[1].query).toBe "COMMIT"
|
||||
expect(@performed[2].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[3].query).toBe "COMMIT"
|
||||
expect(@performed[4].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
||||
expect(@performed[5].query).toBe "COMMIT"
|
300
spec/stores/database-store-spec.es6
Normal file
300
spec/stores/database-store-spec.es6
Normal file
|
@ -0,0 +1,300 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import Thread from '../../src/flux/models/thread';
|
||||
import TestModel from '../fixtures/db-test-model';
|
||||
import ModelQuery from '../../src/flux/models/query';
|
||||
import DatabaseStore from '../../src/flux/stores/database-store';
|
||||
|
||||
const testMatchers = {'id': 'b'};
|
||||
|
||||
describe("DatabaseStore", function DatabaseStoreSpecs() {
|
||||
beforeEach(() => {
|
||||
TestModel.configureBasic();
|
||||
|
||||
DatabaseStore._atomicallyQueue = undefined;
|
||||
DatabaseStore._mutationQueue = undefined;
|
||||
DatabaseStore._inTransaction = false;
|
||||
|
||||
spyOn(ModelQuery.prototype, 'where').andCallThrough();
|
||||
spyOn(DatabaseStore, 'accumulateAndTrigger').andCallFake(() => Promise.resolve());
|
||||
|
||||
this.performed = [];
|
||||
|
||||
// Note: We spy on _query and test all of the convenience methods that sit above
|
||||
// it. None of these tests evaluate whether _query works!
|
||||
jasmine.unspy(DatabaseStore, "_query");
|
||||
spyOn(DatabaseStore, "_query").andCallFake((query, values = []) => {
|
||||
this.performed.push({query, values});
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("find", () =>
|
||||
it("should return a ModelQuery for retrieving a single item by Id", () => {
|
||||
const q = DatabaseStore.find(TestModel, "4");
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = '4' LIMIT 1");
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("findBy", () => {
|
||||
it("should pass the provided predicates on to the ModelQuery", () => {
|
||||
DatabaseStore.findBy(TestModel, testMatchers);
|
||||
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers);
|
||||
});
|
||||
|
||||
it("should return a ModelQuery ready to be executed", () => {
|
||||
const q = DatabaseStore.findBy(TestModel, testMatchers);
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' LIMIT 1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should pass the provided predicates on to the ModelQuery", () => {
|
||||
DatabaseStore.findAll(TestModel, testMatchers);
|
||||
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers);
|
||||
});
|
||||
|
||||
it("should return a ModelQuery ready to be executed", () => {
|
||||
const q = DatabaseStore.findAll(TestModel, testMatchers);
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelify", () => {
|
||||
beforeEach(() => {
|
||||
this.models = [
|
||||
new Thread({clientId: 'local-A'}),
|
||||
new Thread({clientId: 'local-B'}),
|
||||
new Thread({clientId: 'local-C'}),
|
||||
new Thread({clientId: 'local-D', serverId: 'SERVER:D'}),
|
||||
new Thread({clientId: 'local-E', serverId: 'SERVER:E'}),
|
||||
new Thread({clientId: 'local-F', serverId: 'SERVER:F'}),
|
||||
new Thread({clientId: 'local-G', serverId: 'SERVER:G'}),
|
||||
];
|
||||
// Actually returns correct sets for queries, since matchers can evaluate
|
||||
// themselves against models in memory
|
||||
spyOn(DatabaseStore, 'run').andCallFake(query => {
|
||||
const results = this.models.filter(model =>
|
||||
query._matchers.every(matcher => matcher.evaluate(model))
|
||||
);
|
||||
return Promise.resolve(results);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when given an array or input that is not an array", () =>
|
||||
it("resolves immediately with an empty array", () =>
|
||||
waitsForPromise(() => {
|
||||
return DatabaseStore.modelify(Thread, null).then(output => {
|
||||
expect(output).toEqual([]);
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
describe("when given an array of mixed IDs, clientIDs, and models", () =>
|
||||
it("resolves with an array of models", () => {
|
||||
const input = ['SERVER:F', 'local-B', 'local-C', 'SERVER:D', this.models[6]];
|
||||
const expectedOutput = [this.models[5], this.models[1], this.models[2], this.models[3], this.models[6]];
|
||||
return waitsForPromise(() => {
|
||||
return DatabaseStore.modelify(Thread, input).then(output => {
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("when the input is only IDs", () =>
|
||||
it("resolves with an array of models", () => {
|
||||
const input = ['SERVER:D', 'SERVER:F', 'SERVER:G'];
|
||||
const expectedOutput = [this.models[3], this.models[5], this.models[6]];
|
||||
return waitsForPromise(() => {
|
||||
return DatabaseStore.modelify(Thread, input).then(output => {
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("when the input is only clientIDs", () =>
|
||||
it("resolves with an array of models", () => {
|
||||
const input = ['local-A', 'local-B', 'local-C', 'local-D'];
|
||||
const expectedOutput = [this.models[0], this.models[1], this.models[2], this.models[3]];
|
||||
return waitsForPromise(() => {
|
||||
return DatabaseStore.modelify(Thread, input).then(output => {
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("when the input is all models", () =>
|
||||
it("resolves with an array of models", () => {
|
||||
const input = [this.models[0], this.models[1], this.models[2], this.models[3]];
|
||||
const expectedOutput = [this.models[0], this.models[1], this.models[2], this.models[3]];
|
||||
return waitsForPromise(() => {
|
||||
return DatabaseStore.modelify(Thread, input).then(output => {
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
describe("count", () => {
|
||||
it("should pass the provided predicates on to the ModelQuery", () => {
|
||||
DatabaseStore.findAll(TestModel, testMatchers);
|
||||
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers);
|
||||
});
|
||||
|
||||
it("should return a ModelQuery configured for COUNT ready to be executed", () => {
|
||||
const q = DatabaseStore.findAll(TestModel, testMatchers);
|
||||
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inTransaction", () => {
|
||||
it("calls the provided function inside an exclusive transaction", () =>
|
||||
waitsForPromise(() => {
|
||||
return DatabaseStore.inTransaction(() => {
|
||||
return DatabaseStore._query("TEST");
|
||||
}).then(() => {
|
||||
expect(this.performed.length).toBe(3);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("TEST");
|
||||
expect(this.performed[2].query).toBe("COMMIT");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
it("preserves resolved values", () =>
|
||||
waitsForPromise(() => {
|
||||
return DatabaseStore.inTransaction(() => {
|
||||
DatabaseStore._query("TEST");
|
||||
return Promise.resolve("myValue");
|
||||
}).then(myValue => {
|
||||
expect(myValue).toBe("myValue");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
it("always fires a COMMIT, even if the body function fails", () =>
|
||||
waitsForPromise(() => {
|
||||
return DatabaseStore.inTransaction(() => {
|
||||
throw new Error("BOOO");
|
||||
}).catch(() => {
|
||||
expect(this.performed.length).toBe(2);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("COMMIT");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
it("can be called multiple times and get queued", () =>
|
||||
waitsForPromise(() => {
|
||||
return Promise.all([
|
||||
DatabaseStore.inTransaction(() => { }),
|
||||
DatabaseStore.inTransaction(() => { }),
|
||||
DatabaseStore.inTransaction(() => { }),
|
||||
]).then(() => {
|
||||
expect(this.performed.length).toBe(6);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("COMMIT");
|
||||
expect(this.performed[2].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[3].query).toBe("COMMIT");
|
||||
expect(this.performed[4].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[5].query).toBe("COMMIT");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
it("carries on if one of them fails, but still calls the COMMIT for the failed block", () => {
|
||||
let caughtError = false;
|
||||
DatabaseStore.inTransaction(() => DatabaseStore._query("ONE"));
|
||||
DatabaseStore.inTransaction(() => { throw new Error("fail"); }).catch(() => { caughtError = true });
|
||||
DatabaseStore.inTransaction(() => DatabaseStore._query("THREE"));
|
||||
advanceClock(100);
|
||||
expect(this.performed.length).toBe(8);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("ONE");
|
||||
expect(this.performed[2].query).toBe("COMMIT");
|
||||
expect(this.performed[3].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[4].query).toBe("COMMIT");
|
||||
expect(this.performed[5].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[6].query).toBe("THREE");
|
||||
expect(this.performed[7].query).toBe("COMMIT");
|
||||
expect(caughtError).toBe(true);
|
||||
});
|
||||
|
||||
it("is actually running in series and blocks on never-finishing specs", () => {
|
||||
let resolver = null;
|
||||
DatabaseStore.inTransaction(() => { });
|
||||
advanceClock(100);
|
||||
expect(this.performed.length).toBe(2);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("COMMIT");
|
||||
DatabaseStore.inTransaction(() => new Promise((resolve) => { resolver = resolve }));
|
||||
advanceClock(100);
|
||||
let blockedPromiseDone = false;
|
||||
DatabaseStore.inTransaction(() => { }).then(() => {
|
||||
blockedPromiseDone = true;
|
||||
});
|
||||
advanceClock(100);
|
||||
expect(this.performed.length).toBe(3);
|
||||
expect(this.performed[2].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(blockedPromiseDone).toBe(false);
|
||||
|
||||
// Now that we've made our assertion about blocking, we need to clean up
|
||||
// our test and actually resolve that blocked promise now, otherwise
|
||||
// remaining tests won't run properly.
|
||||
advanceClock(100);
|
||||
resolver();
|
||||
advanceClock(100);
|
||||
expect(blockedPromiseDone).toBe(true);
|
||||
return advanceClock(100);
|
||||
});
|
||||
|
||||
it("can be called multiple times and preserve return values", () =>
|
||||
waitsForPromise(() => {
|
||||
let v1 = null;
|
||||
let v2 = null;
|
||||
let v3 = null;
|
||||
return Promise.all([
|
||||
DatabaseStore.inTransaction(() => "a").then(val => { v1 = val }),
|
||||
DatabaseStore.inTransaction(() => "b").then(val => { v2 = val }),
|
||||
DatabaseStore.inTransaction(() => "c").then(val => { v3 = val }),
|
||||
]).then(() => {
|
||||
expect(v1).toBe("a");
|
||||
expect(v2).toBe("b");
|
||||
expect(v3).toBe("c");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
it("can be called multiple times and get queued", () =>
|
||||
waitsForPromise(() => {
|
||||
return DatabaseStore.inTransaction(() => { })
|
||||
.then(() => DatabaseStore.inTransaction(() => { }))
|
||||
.then(() => DatabaseStore.inTransaction(() => { }))
|
||||
.then(() => {
|
||||
expect(this.performed.length).toBe(6);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("COMMIT");
|
||||
expect(this.performed[2].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[3].query).toBe("COMMIT");
|
||||
expect(this.performed[4].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[5].query).toBe("COMMIT");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,299 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
Category = require '../../src/flux/models/category'
|
||||
Thread = require('../../src/flux/models/thread').default
|
||||
TestModel = require '../fixtures/db-test-model'
|
||||
ModelQuery = require('../../src/flux/models/query').default
|
||||
DatabaseTransaction = require('../../src/flux/stores/database-transaction').default
|
||||
|
||||
testMatchers = {'id': 'b'}
|
||||
testModelInstance = new TestModel(id: "1234")
|
||||
testModelInstanceA = new TestModel(id: "AAA")
|
||||
testModelInstanceB = new TestModel(id: "BBB")
|
||||
|
||||
describe "DatabaseTransaction", ->
|
||||
beforeEach ->
|
||||
@databaseMutationHooks = []
|
||||
@performed = []
|
||||
@database =
|
||||
_query: jasmine.createSpy('database._query').andCallFake (query, values=[], options={}) =>
|
||||
@performed.push({query, values})
|
||||
Promise.resolve([])
|
||||
accumulateAndTrigger: jasmine.createSpy('database.accumulateAndTrigger')
|
||||
mutationHooks: => @databaseMutationHooks
|
||||
|
||||
@transaction = new DatabaseTransaction(@database)
|
||||
|
||||
describe "execute", ->
|
||||
|
||||
describe "persistModel", ->
|
||||
it "should throw an exception if the model is not a subclass of Model", ->
|
||||
expect(=> @transaction.persistModel({id: 'asd', subject: 'bla'})).toThrow()
|
||||
|
||||
it "should call through to persistModels", ->
|
||||
spyOn(@transaction, 'persistModels').andReturn Promise.resolve()
|
||||
@transaction.persistModel(testModelInstance)
|
||||
advanceClock()
|
||||
expect(@transaction.persistModels.callCount).toBe(1)
|
||||
|
||||
describe "persistModels", ->
|
||||
it "should call accumulateAndTrigger with a change that contains the models", ->
|
||||
runs =>
|
||||
@transaction.execute (t) =>
|
||||
t.persistModels([testModelInstanceA, testModelInstanceB])
|
||||
waitsFor =>
|
||||
@database.accumulateAndTrigger.callCount > 0
|
||||
runs =>
|
||||
change = @database.accumulateAndTrigger.mostRecentCall.args[0]
|
||||
expect(change).toEqual
|
||||
objectClass: TestModel.name,
|
||||
objectIds: [testModelInstanceA.id, testModelInstanceB.id]
|
||||
objects: [testModelInstanceA, testModelInstanceB]
|
||||
type:'persist'
|
||||
|
||||
it "should call through to _writeModels after checking them", ->
|
||||
spyOn(@transaction, '_writeModels').andReturn Promise.resolve()
|
||||
@transaction.persistModels([testModelInstanceA, testModelInstanceB])
|
||||
advanceClock()
|
||||
expect(@transaction._writeModels.callCount).toBe(1)
|
||||
|
||||
it "should throw an exception if the models are not the same class,\
|
||||
since it cannot be specified by the trigger payload", ->
|
||||
expect(=> @transaction.persistModels([testModelInstanceA, new Category()])).toThrow()
|
||||
|
||||
it "should throw an exception if the models are not a subclass of Model", ->
|
||||
expect(=> @transaction.persistModels([{id: 'asd', subject: 'bla'}])).toThrow()
|
||||
|
||||
describe "mutationHooks", ->
|
||||
beforeEach ->
|
||||
@beforeShouldThrow = false
|
||||
@beforeShouldReject = false
|
||||
|
||||
@hook =
|
||||
beforeDatabaseChange: jasmine.createSpy('beforeDatabaseChange').andCallFake =>
|
||||
throw new Error("beforeShouldThrow") if @beforeShouldThrow
|
||||
new Promise (resolve, reject) =>
|
||||
setTimeout =>
|
||||
return resolve(new Error("beforeShouldReject")) if @beforeShouldReject
|
||||
resolve("value")
|
||||
, 1000
|
||||
afterDatabaseChange: jasmine.createSpy('afterDatabaseChange').andCallFake =>
|
||||
new Promise (resolve, reject) ->
|
||||
setTimeout(( => resolve()), 1000)
|
||||
|
||||
@databaseMutationHooks.push(@hook)
|
||||
|
||||
@writeModelsResolve = null
|
||||
spyOn(@transaction, '_writeModels').andCallFake =>
|
||||
new Promise (resolve, reject) =>
|
||||
@writeModelsResolve = resolve
|
||||
|
||||
it "should run pre-mutation hooks, wait to write models, and then run post-mutation hooks", ->
|
||||
@transaction.persistModels([testModelInstanceA, testModelInstanceB])
|
||||
advanceClock()
|
||||
expect(@hook.beforeDatabaseChange).toHaveBeenCalledWith(
|
||||
@transaction._query,
|
||||
{
|
||||
objects: [testModelInstanceA, testModelInstanceB]
|
||||
objectIds: [testModelInstanceA.id, testModelInstanceB.id]
|
||||
objectClass: testModelInstanceA.constructor.name
|
||||
type: 'persist'
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(@transaction._writeModels).not.toHaveBeenCalled()
|
||||
advanceClock(1100)
|
||||
advanceClock()
|
||||
expect(@transaction._writeModels).toHaveBeenCalled()
|
||||
expect(@hook.afterDatabaseChange).not.toHaveBeenCalled()
|
||||
@writeModelsResolve()
|
||||
advanceClock()
|
||||
advanceClock()
|
||||
expect(@hook.afterDatabaseChange).toHaveBeenCalledWith(
|
||||
@transaction._query,
|
||||
{
|
||||
objects: [testModelInstanceA, testModelInstanceB]
|
||||
objectIds: [testModelInstanceA.id, testModelInstanceB.id]
|
||||
objectClass: testModelInstanceA.constructor.name
|
||||
type: 'persist'
|
||||
},
|
||||
"value"
|
||||
)
|
||||
|
||||
it "should carry on if a pre-mutation hook throws", ->
|
||||
@beforeShouldThrow = true
|
||||
@transaction.persistModels([testModelInstanceA, testModelInstanceB])
|
||||
advanceClock(1000)
|
||||
expect(@hook.beforeDatabaseChange).toHaveBeenCalled()
|
||||
advanceClock()
|
||||
advanceClock()
|
||||
expect(@transaction._writeModels).toHaveBeenCalled()
|
||||
|
||||
it "should carry on if a pre-mutation hook rejects", ->
|
||||
@beforeShouldReject = true
|
||||
@transaction.persistModels([testModelInstanceA, testModelInstanceB])
|
||||
advanceClock(1000)
|
||||
expect(@hook.beforeDatabaseChange).toHaveBeenCalled()
|
||||
advanceClock()
|
||||
advanceClock()
|
||||
expect(@transaction._writeModels).toHaveBeenCalled()
|
||||
|
||||
describe "unpersistModel", ->
|
||||
it "should delete the model by id", ->
|
||||
waitsForPromise =>
|
||||
@transaction.execute =>
|
||||
@transaction.unpersistModel(testModelInstance)
|
||||
.then =>
|
||||
expect(@performed.length).toBe(3)
|
||||
expect(@performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION")
|
||||
expect(@performed[1].query).toBe("DELETE FROM `TestModel` WHERE `id` = ?")
|
||||
expect(@performed[1].values[0]).toBe('1234')
|
||||
expect(@performed[2].query).toBe("COMMIT")
|
||||
|
||||
it "should call accumulateAndTrigger with a change that contains the model", ->
|
||||
runs =>
|
||||
@transaction.execute =>
|
||||
@transaction.unpersistModel(testModelInstance)
|
||||
waitsFor =>
|
||||
@database.accumulateAndTrigger.callCount > 0
|
||||
runs =>
|
||||
change = @database.accumulateAndTrigger.mostRecentCall.args[0]
|
||||
expect(change).toEqual({
|
||||
objectClass: TestModel.name,
|
||||
objectIds: [testModelInstance.id]
|
||||
objects: [testModelInstance],
|
||||
type:'unpersist'
|
||||
})
|
||||
|
||||
describe "when the model has collection attributes", ->
|
||||
it "should delete all of the elements in the join tables", ->
|
||||
TestModel.configureWithCollectionAttribute()
|
||||
waitsForPromise =>
|
||||
@transaction.execute (t) =>
|
||||
t.unpersistModel(testModelInstance)
|
||||
.then =>
|
||||
expect(@performed.length).toBe(4)
|
||||
expect(@performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION")
|
||||
expect(@performed[2].query).toBe("DELETE FROM `TestModelCategory` WHERE `id` = ?")
|
||||
expect(@performed[2].values[0]).toBe('1234')
|
||||
expect(@performed[3].query).toBe("COMMIT")
|
||||
|
||||
describe "when the model has joined data attributes", ->
|
||||
it "should delete the element in the joined data table", ->
|
||||
TestModel.configureWithJoinedDataAttribute()
|
||||
waitsForPromise =>
|
||||
@transaction.execute (t) =>
|
||||
t.unpersistModel(testModelInstance)
|
||||
.then =>
|
||||
expect(@performed.length).toBe(4)
|
||||
expect(@performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION")
|
||||
expect(@performed[2].query).toBe("DELETE FROM `TestModelBody` WHERE `id` = ?")
|
||||
expect(@performed[2].values[0]).toBe('1234')
|
||||
expect(@performed[3].query).toBe("COMMIT")
|
||||
|
||||
describe "_writeModels", ->
|
||||
it "should compose a REPLACE INTO query to save the model", ->
|
||||
TestModel.configureWithCollectionAttribute()
|
||||
@transaction._writeModels([testModelInstance])
|
||||
expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data,client_id,server_id,other) VALUES (?,?,?,?,?)")
|
||||
|
||||
it "should save the model JSON into the data column", ->
|
||||
@transaction._writeModels([testModelInstance])
|
||||
expect(@performed[0].values[1]).toEqual(JSON.stringify(testModelInstance))
|
||||
|
||||
describe "when the model defines additional queryable attributes", ->
|
||||
beforeEach ->
|
||||
TestModel.configureWithAllAttributes()
|
||||
@m = new TestModel
|
||||
id: 'local-6806434c-b0cd'
|
||||
datetime: new Date()
|
||||
string: 'hello world',
|
||||
boolean: true,
|
||||
number: 15
|
||||
|
||||
it "should populate additional columns defined by the attributes", ->
|
||||
@transaction._writeModels([@m])
|
||||
expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data,datetime,string-json-key,boolean,number) VALUES (?,?,?,?,?,?)")
|
||||
|
||||
it "should use the JSON-form values of the queryable attributes", ->
|
||||
json = @m.toJSON()
|
||||
@transaction._writeModels([@m])
|
||||
|
||||
values = @performed[0].values
|
||||
expect(values[2]).toEqual(json['datetime'])
|
||||
expect(values[3]).toEqual(json['string-json-key'])
|
||||
expect(values[4]).toEqual(json['boolean'])
|
||||
expect(values[5]).toEqual(json['number'])
|
||||
|
||||
describe "when the model has collection attributes", ->
|
||||
beforeEach ->
|
||||
TestModel.configureWithCollectionAttribute()
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd', other: 'other')
|
||||
@m.categories = [new Category(id: 'a'),new Category(id: 'b')]
|
||||
@transaction._writeModels([@m])
|
||||
|
||||
it "should delete all association records for the model from join tables", ->
|
||||
expect(@performed[1].query).toBe('DELETE FROM `TestModelCategory` WHERE `id` IN (\'local-6806434c-b0cd\')')
|
||||
|
||||
it "should insert new association records into join tables in a single query, and include queryableBy columns", ->
|
||||
expect(@performed[2].query).toBe('INSERT OR IGNORE INTO `TestModelCategory` (`id`,`value`,`other`) VALUES (?,?,?),(?,?,?)')
|
||||
expect(@performed[2].values).toEqual(['local-6806434c-b0cd', 'a', 'other','local-6806434c-b0cd', 'b', 'other'])
|
||||
|
||||
describe "model collection attributes query building", ->
|
||||
beforeEach ->
|
||||
TestModel.configureWithCollectionAttribute()
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd', other: 'other')
|
||||
@m.categories = []
|
||||
|
||||
it "should page association records into multiple queries correctly", ->
|
||||
@m.categories.push(new Category(id: "id-#{i}")) for i in [0..199]
|
||||
@transaction._writeModels([@m])
|
||||
|
||||
collectionAttributeQueries = _.filter @performed, (i) ->
|
||||
i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') == 0
|
||||
|
||||
expect(collectionAttributeQueries.length).toBe(1)
|
||||
expect(collectionAttributeQueries[0].values[200*3-2]).toEqual('id-199')
|
||||
|
||||
it "should page association records into multiple queries correctly", ->
|
||||
@m.categories.push(new Category(id: "id-#{i}")) for i in [0..200]
|
||||
@transaction._writeModels([@m])
|
||||
|
||||
collectionAttributeQueries = _.filter @performed, (i) ->
|
||||
i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') == 0
|
||||
|
||||
expect(collectionAttributeQueries.length).toBe(2)
|
||||
expect(collectionAttributeQueries[0].values[200*3-2]).toEqual('id-199')
|
||||
expect(collectionAttributeQueries[1].values[1]).toEqual('id-200')
|
||||
|
||||
it "should page association records into multiple queries correctly", ->
|
||||
@m.categories.push(new Category(id: "id-#{i}")) for i in [0..201]
|
||||
@transaction._writeModels([@m])
|
||||
|
||||
collectionAttributeQueries = _.filter @performed, (i) ->
|
||||
i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') == 0
|
||||
|
||||
expect(collectionAttributeQueries.length).toBe(2)
|
||||
expect(collectionAttributeQueries[0].values[200*3-2]).toEqual('id-199')
|
||||
expect(collectionAttributeQueries[1].values[1]).toEqual('id-200')
|
||||
expect(collectionAttributeQueries[1].values[4]).toEqual('id-201')
|
||||
|
||||
describe "when the model has joined data attributes", ->
|
||||
beforeEach ->
|
||||
TestModel.configureWithJoinedDataAttribute()
|
||||
|
||||
it "should not include the value to the joined attribute in the JSON written to the main model table", ->
|
||||
@m = new TestModel(clientId: 'local-6806434c-b0cd', serverId: 'server-1', body: 'hello world')
|
||||
@transaction._writeModels([@m])
|
||||
expect(@performed[0].values).toEqual(['server-1', '{"client_id":"local-6806434c-b0cd","server_id":"server-1","id":"server-1"}', 'local-6806434c-b0cd', 'server-1'])
|
||||
|
||||
it "should write the value to the joined table if it is defined", ->
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
|
||||
@transaction._writeModels([@m])
|
||||
expect(@performed[1].query).toBe('REPLACE INTO `TestModelBody` (`id`, `value`) VALUES (?, ?)')
|
||||
expect(@performed[1].values).toEqual([@m.id, @m.body])
|
||||
|
||||
it "should not write the value to the joined table if it undefined", ->
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd')
|
||||
@transaction._writeModels([@m])
|
||||
expect(@performed.length).toBe(1)
|
387
spec/stores/database-transaction-spec.es6
Normal file
387
spec/stores/database-transaction-spec.es6
Normal file
|
@ -0,0 +1,387 @@
|
|||
/* eslint dot-notation:0 */
|
||||
import Category from '../../src/flux/models/category';
|
||||
import TestModel from '../fixtures/db-test-model';
|
||||
import DatabaseTransaction from '../../src/flux/stores/database-transaction';
|
||||
|
||||
const testModelInstance = new TestModel({id: "1234"});
|
||||
const testModelInstanceA = new TestModel({id: "AAA"});
|
||||
const testModelInstanceB = new TestModel({id: "BBB"});
|
||||
|
||||
function __range__(left, right, inclusive) {
|
||||
const range = [];
|
||||
const ascending = left < right;
|
||||
const incr = ascending ? right + 1 : right - 1;
|
||||
const end = !inclusive ? right : incr;
|
||||
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
|
||||
range.push(i);
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
describe("DatabaseTransaction", function DatabaseTransactionSpecs() {
|
||||
beforeEach(() => {
|
||||
this.databaseMutationHooks = [];
|
||||
this.performed = [];
|
||||
this.database = {
|
||||
_query: jasmine.createSpy('database._query').andCallFake((query, values = []) => {
|
||||
this.performed.push({query, values});
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
accumulateAndTrigger: jasmine.createSpy('database.accumulateAndTrigger'),
|
||||
mutationHooks: () => this.databaseMutationHooks,
|
||||
};
|
||||
|
||||
this.transaction = new DatabaseTransaction(this.database);
|
||||
});
|
||||
|
||||
describe("execute", () => {});
|
||||
|
||||
describe("persistModel", () => {
|
||||
it("should throw an exception if the model is not a subclass of Model", () => expect(() => this.transaction.persistModel({id: 'asd', subject: 'bla'})).toThrow()
|
||||
);
|
||||
|
||||
it("should call through to persistModels", () => {
|
||||
spyOn(this.transaction, 'persistModels').andReturn(Promise.resolve());
|
||||
this.transaction.persistModel(testModelInstance);
|
||||
advanceClock();
|
||||
expect(this.transaction.persistModels.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("persistModels", () => {
|
||||
it("should call accumulateAndTrigger with a change that contains the models", () => {
|
||||
runs(() => {
|
||||
return this.transaction.execute(t => {
|
||||
return t.persistModels([testModelInstanceA, testModelInstanceB]);
|
||||
});
|
||||
});
|
||||
waitsFor(() => {
|
||||
return this.database.accumulateAndTrigger.callCount > 0;
|
||||
});
|
||||
runs(() => {
|
||||
const change = this.database.accumulateAndTrigger.mostRecentCall.args[0];
|
||||
expect(change).toEqual({
|
||||
objectClass: TestModel.name,
|
||||
objectIds: [testModelInstanceA.id, testModelInstanceB.id],
|
||||
objects: [testModelInstanceA, testModelInstanceB],
|
||||
type: 'persist',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should call through to _writeModels after checking them", () => {
|
||||
spyOn(this.transaction, '_writeModels').andReturn(Promise.resolve());
|
||||
this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);
|
||||
advanceClock();
|
||||
expect(this.transaction._writeModels.callCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should throw an exception if the models are not the same class, since it cannot be specified by the trigger payload", () =>
|
||||
expect(() => this.transaction.persistModels([testModelInstanceA, new Category()])).toThrow()
|
||||
);
|
||||
|
||||
it("should throw an exception if the models are not a subclass of Model", () =>
|
||||
expect(() => this.transaction.persistModels([{id: 'asd', subject: 'bla'}])).toThrow()
|
||||
);
|
||||
|
||||
describe("mutationHooks", () => {
|
||||
beforeEach(() => {
|
||||
this.beforeShouldThrow = false;
|
||||
this.beforeShouldReject = false;
|
||||
|
||||
this.hook = {
|
||||
beforeDatabaseChange: jasmine.createSpy('beforeDatabaseChange').andCallFake(() => {
|
||||
if (this.beforeShouldThrow) { throw new Error("beforeShouldThrow"); }
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (this.beforeShouldReject) { resolve(new Error("beforeShouldReject")); }
|
||||
resolve("value");
|
||||
}
|
||||
, 1000);
|
||||
});
|
||||
}),
|
||||
afterDatabaseChange: jasmine.createSpy('afterDatabaseChange').andCallFake(() => {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(), 1000));
|
||||
}),
|
||||
};
|
||||
|
||||
this.databaseMutationHooks.push(this.hook);
|
||||
|
||||
this.writeModelsResolve = null;
|
||||
spyOn(this.transaction, '_writeModels').andCallFake(() => {
|
||||
return new Promise((resolve) => {
|
||||
this.writeModelsResolve = resolve;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should run pre-mutation hooks, wait to write models, and then run post-mutation hooks", () => {
|
||||
this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);
|
||||
advanceClock();
|
||||
expect(this.hook.beforeDatabaseChange).toHaveBeenCalledWith(
|
||||
this.transaction._query,
|
||||
{
|
||||
objects: [testModelInstanceA, testModelInstanceB],
|
||||
objectIds: [testModelInstanceA.id, testModelInstanceB.id],
|
||||
objectClass: testModelInstanceA.constructor.name,
|
||||
type: 'persist',
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(this.transaction._writeModels).not.toHaveBeenCalled();
|
||||
advanceClock(1100);
|
||||
advanceClock();
|
||||
expect(this.transaction._writeModels).toHaveBeenCalled();
|
||||
expect(this.hook.afterDatabaseChange).not.toHaveBeenCalled();
|
||||
this.writeModelsResolve();
|
||||
advanceClock();
|
||||
advanceClock();
|
||||
expect(this.hook.afterDatabaseChange).toHaveBeenCalledWith(
|
||||
this.transaction._query,
|
||||
{
|
||||
objects: [testModelInstanceA, testModelInstanceB],
|
||||
objectIds: [testModelInstanceA.id, testModelInstanceB.id],
|
||||
objectClass: testModelInstanceA.constructor.name,
|
||||
type: 'persist',
|
||||
},
|
||||
"value"
|
||||
);
|
||||
});
|
||||
|
||||
it("should carry on if a pre-mutation hook throws", () => {
|
||||
this.beforeShouldThrow = true;
|
||||
this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);
|
||||
advanceClock(1000);
|
||||
expect(this.hook.beforeDatabaseChange).toHaveBeenCalled();
|
||||
advanceClock();
|
||||
advanceClock();
|
||||
expect(this.transaction._writeModels).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should carry on if a pre-mutation hook rejects", () => {
|
||||
this.beforeShouldReject = true;
|
||||
this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);
|
||||
advanceClock(1000);
|
||||
expect(this.hook.beforeDatabaseChange).toHaveBeenCalled();
|
||||
advanceClock();
|
||||
advanceClock();
|
||||
expect(this.transaction._writeModels).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("unpersistModel", () => {
|
||||
it("should delete the model by id", () =>
|
||||
waitsForPromise(() => {
|
||||
return this.transaction.execute(() => {
|
||||
return this.transaction.unpersistModel(testModelInstance);
|
||||
})
|
||||
.then(() => {
|
||||
expect(this.performed.length).toBe(3);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[1].query).toBe("DELETE FROM `TestModel` WHERE `id` = ?");
|
||||
expect(this.performed[1].values[0]).toBe('1234');
|
||||
expect(this.performed[2].query).toBe("COMMIT");
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
it("should call accumulateAndTrigger with a change that contains the model", () => {
|
||||
runs(() => {
|
||||
return this.transaction.execute(() => {
|
||||
return this.transaction.unpersistModel(testModelInstance);
|
||||
});
|
||||
});
|
||||
waitsFor(() => {
|
||||
return this.database.accumulateAndTrigger.callCount > 0;
|
||||
});
|
||||
runs(() => {
|
||||
const change = this.database.accumulateAndTrigger.mostRecentCall.args[0];
|
||||
expect(change).toEqual({
|
||||
objectClass: TestModel.name,
|
||||
objectIds: [testModelInstance.id],
|
||||
objects: [testModelInstance],
|
||||
type: 'unpersist',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the model has collection attributes", () =>
|
||||
it("should delete all of the elements in the join tables", () => {
|
||||
TestModel.configureWithCollectionAttribute();
|
||||
waitsForPromise(() => {
|
||||
return this.transaction.execute(t => {
|
||||
return t.unpersistModel(testModelInstance);
|
||||
})
|
||||
.then(() => {
|
||||
expect(this.performed.length).toBe(4);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[2].query).toBe("DELETE FROM `TestModelCategory` WHERE `id` = ?");
|
||||
expect(this.performed[2].values[0]).toBe('1234');
|
||||
expect(this.performed[3].query).toBe("COMMIT");
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
|
||||
describe("when the model has joined data attributes", () =>
|
||||
it("should delete the element in the joined data table", () => {
|
||||
TestModel.configureWithJoinedDataAttribute();
|
||||
waitsForPromise(() => {
|
||||
return this.transaction.execute(t => {
|
||||
return t.unpersistModel(testModelInstance);
|
||||
})
|
||||
.then(() => {
|
||||
expect(this.performed.length).toBe(4);
|
||||
expect(this.performed[0].query).toBe("BEGIN IMMEDIATE TRANSACTION");
|
||||
expect(this.performed[2].query).toBe("DELETE FROM `TestModelBody` WHERE `id` = ?");
|
||||
expect(this.performed[2].values[0]).toBe('1234');
|
||||
expect(this.performed[3].query).toBe("COMMIT");
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
describe("_writeModels", () => {
|
||||
it("should compose a REPLACE INTO query to save the model", () => {
|
||||
TestModel.configureWithCollectionAttribute();
|
||||
this.transaction._writeModels([testModelInstance]);
|
||||
expect(this.performed[0].query).toBe("REPLACE INTO `TestModel` (id,data,client_id,server_id,other) VALUES (?,?,?,?,?)");
|
||||
});
|
||||
|
||||
it("should save the model JSON into the data column", () => {
|
||||
this.transaction._writeModels([testModelInstance]);
|
||||
expect(this.performed[0].values[1]).toEqual(JSON.stringify(testModelInstance));
|
||||
});
|
||||
|
||||
describe("when the model defines additional queryable attributes", () => {
|
||||
beforeEach(() => {
|
||||
TestModel.configureWithAllAttributes();
|
||||
this.m = new TestModel({
|
||||
'id': 'local-6806434c-b0cd',
|
||||
'datetime': new Date(),
|
||||
'string': 'hello world',
|
||||
'boolean': true,
|
||||
'number': 15,
|
||||
});
|
||||
});
|
||||
|
||||
it("should populate additional columns defined by the attributes", () => {
|
||||
this.transaction._writeModels([this.m]);
|
||||
expect(this.performed[0].query).toBe("REPLACE INTO `TestModel` (id,data,datetime,string-json-key,boolean,number) VALUES (?,?,?,?,?,?)");
|
||||
});
|
||||
|
||||
it("should use the JSON-form values of the queryable attributes", () => {
|
||||
const json = this.m.toJSON();
|
||||
this.transaction._writeModels([this.m]);
|
||||
|
||||
const { values } = this.performed[0];
|
||||
expect(values[2]).toEqual(json['datetime']);
|
||||
expect(values[3]).toEqual(json['string-json-key']);
|
||||
expect(values[4]).toEqual(json['boolean']);
|
||||
expect(values[5]).toEqual(json['number']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the model has collection attributes", () => {
|
||||
beforeEach(() => {
|
||||
TestModel.configureWithCollectionAttribute();
|
||||
this.m = new TestModel({id: 'local-6806434c-b0cd', other: 'other'});
|
||||
this.m.categories = [new Category({id: 'a'}), new Category({id: 'b'})];
|
||||
this.transaction._writeModels([this.m]);
|
||||
});
|
||||
|
||||
it("should delete all association records for the model from join tables", () => {
|
||||
expect(this.performed[1].query).toBe('DELETE FROM `TestModelCategory` WHERE `id` IN (\'local-6806434c-b0cd\')');
|
||||
});
|
||||
|
||||
it("should insert new association records into join tables in a single query, and include queryableBy columns", () => {
|
||||
expect(this.performed[2].query).toBe('INSERT OR IGNORE INTO `TestModelCategory` (`id`,`value`,`other`) VALUES (?,?,?),(?,?,?)');
|
||||
expect(this.performed[2].values).toEqual(['local-6806434c-b0cd', 'a', 'other', 'local-6806434c-b0cd', 'b', 'other']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("model collection attributes query building", () => {
|
||||
beforeEach(() => {
|
||||
TestModel.configureWithCollectionAttribute();
|
||||
this.m = new TestModel({id: 'local-6806434c-b0cd', other: 'other'});
|
||||
this.m.categories = [];
|
||||
});
|
||||
|
||||
it("should page association records into multiple queries correctly", () => {
|
||||
const iterable = __range__(0, 199, true);
|
||||
for (let j = 0; j < iterable.length; j++) {
|
||||
const i = iterable[j];
|
||||
this.m.categories.push(new Category({id: `id-${i}`}));
|
||||
}
|
||||
this.transaction._writeModels([this.m]);
|
||||
|
||||
const collectionAttributeQueries = this.performed.filter(i => i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') === 0
|
||||
);
|
||||
|
||||
expect(collectionAttributeQueries.length).toBe(1);
|
||||
expect(collectionAttributeQueries[0].values[(200 * 3) - 2]).toEqual('id-199');
|
||||
});
|
||||
|
||||
it("should page association records into multiple queries correctly", () => {
|
||||
const iterable = __range__(0, 200, true);
|
||||
for (let j = 0; j < iterable.length; j++) {
|
||||
const i = iterable[j];
|
||||
this.m.categories.push(new Category({id: `id-${i}`}));
|
||||
}
|
||||
this.transaction._writeModels([this.m]);
|
||||
|
||||
const collectionAttributeQueries = this.performed.filter(i => i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') === 0
|
||||
);
|
||||
|
||||
expect(collectionAttributeQueries.length).toBe(2);
|
||||
expect(collectionAttributeQueries[0].values[(200 * 3) - 2]).toEqual('id-199');
|
||||
expect(collectionAttributeQueries[1].values[1]).toEqual('id-200');
|
||||
});
|
||||
|
||||
it("should page association records into multiple queries correctly", () => {
|
||||
const iterable = __range__(0, 201, true);
|
||||
for (let j = 0; j < iterable.length; j++) {
|
||||
const i = iterable[j];
|
||||
this.m.categories.push(new Category({id: `id-${i}`}));
|
||||
}
|
||||
this.transaction._writeModels([this.m]);
|
||||
|
||||
const collectionAttributeQueries = this.performed.filter(i => i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') === 0
|
||||
);
|
||||
|
||||
expect(collectionAttributeQueries.length).toBe(2);
|
||||
expect(collectionAttributeQueries[0].values[(200 * 3) - 2]).toEqual('id-199');
|
||||
expect(collectionAttributeQueries[1].values[1]).toEqual('id-200');
|
||||
expect(collectionAttributeQueries[1].values[4]).toEqual('id-201');
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the model has joined data attributes", () => {
|
||||
beforeEach(() => TestModel.configureWithJoinedDataAttribute());
|
||||
|
||||
it("should not include the value to the joined attribute in the JSON written to the main model table", () => {
|
||||
this.m = new TestModel({clientId: 'local-6806434c-b0cd', serverId: 'server-1', body: 'hello world'});
|
||||
this.transaction._writeModels([this.m]);
|
||||
expect(this.performed[0].values).toEqual(['server-1', '{"client_id":"local-6806434c-b0cd","server_id":"server-1","id":"server-1"}', 'local-6806434c-b0cd', 'server-1']);
|
||||
});
|
||||
|
||||
it("should write the value to the joined table if it is defined", () => {
|
||||
this.m = new TestModel({id: 'local-6806434c-b0cd', body: 'hello world'});
|
||||
this.transaction._writeModels([this.m]);
|
||||
expect(this.performed[1].query).toBe('REPLACE INTO `TestModelBody` (`id`, `value`) VALUES (?, ?)');
|
||||
expect(this.performed[1].values).toEqual([this.m.id, this.m.body]);
|
||||
});
|
||||
|
||||
it("should not write the value to the joined table if it undefined", () => {
|
||||
this.m = new TestModel({id: 'local-6806434c-b0cd'});
|
||||
this.transaction._writeModels([this.m]);
|
||||
expect(this.performed.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue