mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-24 17:26:06 +08:00
3ba6c7c59a
Summary: Debounce changes out of the DatabaseStore to prevent lots of calls to persistModel from flooding the app Tasks must always call super so they get IDs The task queue shouldn't save every time it adds/removes a task - there could be hundreds ActivityBar package is actually surprisingly slow, re-rendering needlessly setState in MultiselectList sometimes renders immediately. Don't do this, because sometimes we're rendering twice back to back Remove dead references Never allow duplicate tags in the tags array Don't archive threads that already have the archive tag (it doesn't do anything bad, but why bother creating tasks?) Update DB specs Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1506
278 lines
12 KiB
CoffeeScript
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, 'triggerSoon')
|
|
|
|
@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.triggerSoon).toHaveBeenCalled()
|
|
|
|
change = DatabaseStore.triggerSoon.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.triggerSoon).toHaveBeenCalled()
|
|
|
|
change = DatabaseStore.triggerSoon.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.triggerSoon).toHaveBeenCalled()
|
|
|
|
change = DatabaseStore.triggerSoon.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 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 = DatabaseStore.queriesForTableSetup(TestModel)
|
|
expected = [
|
|
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB)',
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',
|
|
'CREATE TABLE IF NOT EXISTS `TestModel-Tag` (id TEXT KEY, `value` TEXT)'
|
|
'CREATE UNIQUE 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 OR IGNORE 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)
|