Mailspring/spec-inbox/stores/database-store-spec.coffee
Ben Gotow 8f2211f6a0 feat(threads): List improvements and message collapsing
Summary:
This diff uses the new ?expanded=true threads request to fetch threads and the messages inside them at the same time. The messages from this endpoint don't contain bodies. Message bodies have been moved to a new "secondary attribute" type, which can be optionally requested when making queries. This allows us to 1) quickly fetch messages without worrying about MBs of JSON, 2) update messages without updating their bodies, and 3) avoid calls to /messages?thread_id=123. The new message store fetches just the items it wants to display in expanded mode, and we'll show snippets for the rest.

Fix up forwarded message

Approach: Thread.messageMetadata

join approach WIP

join approach complete

"" || null = null. OMG.

Make spinner a bit smarter, use code delays and not css delays

Search suggestion store should only show first 10 matches

Msg collapsing, refactored msg store that will fetch individual messages that are marked as expanded, set loaded = true when it's all done

Test Plan: Tests coming soon. The query refactoring here broke a lot of tests...

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1345
2015-03-25 12:41:48 -07:00

278 lines
12 KiB
CoffeeScript

DatabaseStore = require '../../src/flux/stores/database-store'
Model = require '../../src/flux/models/model'
ModelQuery = require '../../src/flux/models/query'
Attributes = require '../../src/flux/attributes'
Tag = require '../../src/flux/models/tag'
_ = require 'underscore-plus'
class TestModel extends Model
@attributes =
'id': Attributes.String
queryable: true
modelKey: 'id'
TestModel.configureWithAllAttributes = ->
TestModel.attributes =
'datetime': Attributes.DateTime
queryable: true
modelKey: 'datetime'
'string': Attributes.String
queryable: true
modelKey: 'string'
jsonKey: 'string-json-key'
'boolean': Attributes.Boolean
queryable: true
modelKey: 'boolean'
'number': Attributes.Number
queryable: true
modelKey: 'number'
'other': Attributes.String
modelKey: 'other'
TestModel.configureWithCollectionAttribute = ->
TestModel.attributes =
'id': Attributes.String
queryable: true
modelKey: 'id'
'tags': Attributes.Collection
queryable: true
modelKey: 'tags'
itemClass: Tag
TestModel.configureWithJoinedDataAttribute = ->
TestModel.attributes =
'id': Attributes.String
queryable: true
modelKey: 'id'
'body': Attributes.JoinedData
modelTable: 'TestModelBody'
modelKey: 'body'
testMatchers = {'id': 'b'}
testModelInstance = new TestModel(id: '1234')
testModelInstanceA = new TestModel(id: 'AAA')
testModelInstanceB = new TestModel(id: 'BBB')
describe "DatabaseStore", ->
beforeEach ->
spyOn(ModelQuery.prototype, 'where').andCallThrough()
spyOn(DatabaseStore, 'trigger').andCallThrough()
@performed = []
@transactionCount = 0
# Pass spyTx() to functions that take a tx reference to log
# performed queries to the @performed array.
@spyTx = ->
execute: (query, values, success) =>
@performed.push({query: query, values: values})
success() if success
# Spy on the DatabaseStore and return our use spyTx() to generate
# new transactions instead of using the real websql transaction.
spyOn(DatabaseStore, 'inTransaction').andCallFake (options, callback) =>
@transactionCount += 1
callback(@spyTx())
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 "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 "persistModel", ->
it "should cause the DatabaseStore to trigger with a change that contains the model", ->
DatabaseStore.persistModel(testModelInstance)
expect(DatabaseStore.trigger).toHaveBeenCalled()
change = DatabaseStore.trigger.mostRecentCall.args[0]
expect(change).toEqual({objectClass: TestModel.name, objects: [testModelInstance]})
it "should call through to writeModels", ->
spyOn(DatabaseStore, 'writeModels')
DatabaseStore.persistModel(testModelInstance)
expect(DatabaseStore.writeModels.callCount).toBe(1)
describe "persistModels", ->
it "should cause the DatabaseStore to trigger with a change that contains the models", ->
DatabaseStore.persistModels([testModelInstanceA, testModelInstanceB])
expect(DatabaseStore.trigger).toHaveBeenCalled()
change = DatabaseStore.trigger.mostRecentCall.args[0]
expect(change).toEqual
objectClass: TestModel.name,
objects: [testModelInstanceA, testModelInstanceB]
it "should call through to writeModels after checking them", ->
spyOn(DatabaseStore, 'writeModels')
DatabaseStore.persistModels([testModelInstanceA, testModelInstanceB])
expect(DatabaseStore.writeModels.callCount).toBe(1)
it "should only open one database transaction to write all the models", ->
DatabaseStore.persistModels([testModelInstanceA, testModelInstanceB])
expect(@transactionCount).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(-> DatabaseStore.persistModels([testModelInstanceA, new Tag()])).toThrow()
describe "unpersistModel", ->
it "should delete the model by Id", ->
DatabaseStore.unpersistModel(testModelInstance)
expect(@performed.length).toBe(3)
expect(@performed[1].query).toBe("DELETE FROM `TestModel` WHERE `id` = ?")
expect(@performed[1].values[0]).toBe('1234')
it "should cause the DatabaseStore to trigger() with a change that contains the model", ->
DatabaseStore.unpersistModel(testModelInstance)
expect(DatabaseStore.trigger).toHaveBeenCalled()
change = DatabaseStore.trigger.mostRecentCall.args[0]
expect(change).toEqual({objectClass: TestModel.name, objects: [testModelInstance]})
describe "when the model has collection attributes", ->
it "should delete all of the elements in the join tables", ->
TestModel.configureWithCollectionAttribute()
DatabaseStore.unpersistModel(testModelInstance)
expect(@performed.length).toBe(4)
expect(@performed[2].query).toBe("DELETE FROM `TestModel-Tag` WHERE `id` = ?")
expect(@performed[2].values[0]).toBe('1234')
describe "when the model has joined data attributes", ->
it "should delete the element in the joined data table", ->
TestModel.configureWithJoinedDataAttribute()
DatabaseStore.unpersistModel(testModelInstance)
expect(@performed.length).toBe(4)
expect(@performed[2].query).toBe("DELETE FROM `TestModelBody` WHERE `id` = ?")
expect(@performed[2].values[0]).toBe('1234')
describe "queriesForTableSetup", ->
it "should return the queries for creating the table and indexes on queryable columns", ->
TestModel.attributes =
'attrQueryable': Attributes.DateTime
queryable: true
modelKey: 'attrQueryable'
jsonKey: 'attr_queryable'
'attrNonQueryable': Attributes.Collection
modelKey: 'attrNonQueryable'
jsonKey: 'attr_non_queryable'
queries = DatabaseStore.queriesForTableSetup(TestModel)
expected = [
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,attr_queryable INTEGER)',
'CREATE INDEX IF NOT EXISTS `TestModel-attr_queryable` ON `TestModel` (`attr_queryable`)',
'CREATE 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 = DatabaseStore.queriesForTableSetup(TestModel)
expected = [
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB)',
'CREATE INDEX IF NOT EXISTS `TestModel-id` ON `TestModel` (`id`)',
'CREATE TABLE IF NOT EXISTS `TestModel-Tag` (id TEXT KEY, `value` TEXT)'
'CREATE INDEX IF NOT EXISTS `TestModel-Tag-id-val` ON `TestModel-Tag` (`id`,`value`)',
]
for query,i in queries
expect(query).toBe(expected[i])
it "should use the correct column type for each attribute", ->
TestModel.configureWithAllAttributes()
queries = DatabaseStore.queriesForTableSetup(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 "writeModels", ->
it "should compose a REPLACE INTO query to save the model", ->
TestModel.configureWithCollectionAttribute()
DatabaseStore.writeModels(@spyTx(), [testModelInstance])
expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data) VALUES (?,?)")
it "should save the model JSON into the data column", ->
DatabaseStore.writeModels(@spyTx(), [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", ->
DatabaseStore.writeModels(@spyTx(), [@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()
DatabaseStore.writeModels(@spyTx(), [@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')
@m.tags = [new Tag(id: 'a'),new Tag(id: 'b')]
DatabaseStore.writeModels(@spyTx(), [@m])
it "should delete all association records for the model from join tables", ->
expect(@performed[1].query).toBe('DELETE FROM `TestModel-Tag` WHERE `id` IN (\'local-6806434c-b0cd\')')
it "should insert new association records into join tables in a single query", ->
expect(@performed[2].query).toBe('INSERT INTO `TestModel-Tag` (`id`, `value`) VALUES (?,?),(?,?)')
expect(@performed[2].values).toEqual(['local-6806434c-b0cd', 'a','local-6806434c-b0cd', 'b'])
describe "when the model has joined data attributes", ->
beforeEach ->
TestModel.configureWithJoinedDataAttribute()
it "should write the value to the joined table if it is defined", ->
@m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
DatabaseStore.writeModels(@spyTx(), [@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 valeu to the joined table if it undefined", ->
@m = new TestModel(id: 'local-6806434c-b0cd')
DatabaseStore.writeModels(@spyTx(), [@m])
expect(@performed.length).toBe(1)