mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-16 09:35:00 +08:00
feat(tasks): add Create, Update, Destroy tasks plus spec & lint fixes
Summary: 1. **Generic CUD Tasks**: There is now a generic `CreateModelTask`, `UpdateModelTask`, and `DestroyModelTask`. These can either be used as-is or trivially overridden to easily update simple objects. Hopefully all of the boilerplate rollback, error handling, and undo logic won't have to be re-duplicated on every task. There are also tests for these tasks. We use them to perform mutating actions on `Metadata` objects. 1. **Failing on Promise Rejects**: Turns out that if a Promise rejected due to an error or `Promise.reject` we were ignoring it and letting tests pass. Now, tests will Fail if any unhandled promise rejects. This uncovered a variety of errors throughout the test suite that had to be fixed. The most significant one was during the `theme-manager` tests when all packages (and their stores with async DB requests) was loaded. Long after the `theme-manager` specs finished, those DB requests were (somtimes) silently failing. 1. **Globally stub `DatabaseStore._query`**: All tests shouldn't actually make queries on the database. Furthremore, the `inTransaction` block doesn't resolve at all unless `_query` is stubbed. Instead of manually remembering to do this in every test that touches the DB, it's now mocked in `spec_helper`. This broke a handful of tests that needed to be manually fixed. 1. **ESLint Fixes**: Some minor fixes to the linter config to prevent yelling about minor ES6 things and ensuring we have the correct parser. Test Plan: new tests Reviewers: bengotow, juan, drew Differential Revision: https://phab.nylas.com/D2419 Remove cloudState and N1-Send-Later
This commit is contained in:
parent
da54fa7e29
commit
6695de4187
30 changed files with 755 additions and 182 deletions
|
@ -13,8 +13,13 @@
|
|||
"react/prop-types": [2, {"ignore": ["children"]}],
|
||||
"eqeqeq": [2, "smart"],
|
||||
"id-length": [0],
|
||||
"object-curly-spacing": [0],
|
||||
"no-console": [0],
|
||||
"no-loop-func": [0],
|
||||
"new-cap": [2, {"capIsNew": false}],
|
||||
"no-shadow": [1]
|
||||
}
|
||||
"no-shadow": [1],
|
||||
"quotes": [0],
|
||||
"semi": [0]
|
||||
},
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ describe "ComposerView", ->
|
|||
|
||||
describe "A blank composer view", ->
|
||||
beforeEach ->
|
||||
useDraft.call(@)
|
||||
@composer = ReactTestUtils.renderIntoDocument(
|
||||
<ComposerView draftClientId="test123" />
|
||||
)
|
||||
|
|
|
@ -25,6 +25,8 @@ describe "Composer Quoted Text", ->
|
|||
@htmlNoQuote = 'Test <strong>HTML</strong><br>'
|
||||
@htmlWithQuote = 'Test <strong>HTML</strong><br><blockquote class="gmail_quote">QUOTE</blockquote>'
|
||||
|
||||
spyOn(Composer.prototype, "_prepareForDraft")
|
||||
|
||||
@composer = ReactTestUtils.renderIntoDocument(<Composer draftClientId="unused"/>)
|
||||
@composer._proxy = trigger: ->
|
||||
spyOn(@composer, "_addToProxy")
|
||||
|
|
|
@ -102,7 +102,6 @@ describe "NylasSyncWorkerPool", ->
|
|||
"id": @thread.id,
|
||||
"timestamp": "2015-08-26T17:36:45.297Z"
|
||||
|
||||
spyOn(DatabaseTransaction.prototype, '_query').andCallFake -> Promise.resolve([])
|
||||
spyOn(DatabaseTransaction.prototype, 'unpersistModel')
|
||||
|
||||
it "should resolve if the object cannot be found", ->
|
||||
|
|
|
@ -7,9 +7,6 @@ DatabaseStore = require '../src/flux/stores/database-store'
|
|||
DatabaseTransaction = require '../src/flux/stores/database-transaction'
|
||||
|
||||
describe "NylasAPI", ->
|
||||
beforeEach ->
|
||||
spyOn(DatabaseStore, '_query').andCallFake => Promise.resolve([])
|
||||
|
||||
describe "handleModel404", ->
|
||||
it "should unpersist the model from the cache that was requested", ->
|
||||
model = new Thread(id: 'threadidhere')
|
||||
|
|
|
@ -116,6 +116,16 @@ beforeEach ->
|
|||
|
||||
DatabaseStore._transactionQueue = undefined
|
||||
|
||||
## If we don't spy on DatabaseStore._query, then
|
||||
#`DatabaseStore.inTransaction` will never complete and cause all tests
|
||||
#that depend on transactions to hang.
|
||||
#
|
||||
# @_query("BEGIN IMMEDIATE TRANSACTION") never resolves because
|
||||
# DatabaseStore._query never runs because the @_open flag is always
|
||||
# false because we never setup the DB when `NylasEnv.inSpecMode` is
|
||||
# true.
|
||||
spyOn(DatabaseStore, '_query').andCallFake => Promise.resolve([])
|
||||
|
||||
TaskQueue._queue = []
|
||||
TaskQueue._completed = []
|
||||
TaskQueue._onlineStatus = true
|
||||
|
|
|
@ -26,6 +26,7 @@ describe "DatabaseStore", ->
|
|||
|
||||
# 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([])
|
||||
|
|
|
@ -128,6 +128,7 @@ describe "DraftStoreProxy", ->
|
|||
|
||||
describe "teardown", ->
|
||||
it "should mark the session as destroyed", ->
|
||||
spyOn(DraftStoreProxy.prototype, "prepare")
|
||||
proxy = new DraftStoreProxy('client-id')
|
||||
proxy.teardown()
|
||||
expect(proxy._destroyed).toEqual(true)
|
||||
|
@ -135,11 +136,12 @@ describe "DraftStoreProxy", ->
|
|||
describe "prepare", ->
|
||||
beforeEach ->
|
||||
@draft = new Message(draft: true, body: '123', clientId: 'client-id')
|
||||
spyOn(DraftStoreProxy.prototype, "prepare")
|
||||
@proxy = new DraftStoreProxy('client-id')
|
||||
spyOn(@proxy, '_setDraft')
|
||||
spyOn(DatabaseStore, 'run').andCallFake =>
|
||||
spyOn(DatabaseStore, 'run').andCallFake (modelQuery) =>
|
||||
Promise.resolve(@draft)
|
||||
@proxy._draftPromise = null
|
||||
jasmine.unspy(DraftStoreProxy.prototype, "prepare")
|
||||
|
||||
it "should call setDraft with the retrieved draft", ->
|
||||
waitsForPromise =>
|
||||
|
@ -174,7 +176,6 @@ describe "DraftStoreProxy", ->
|
|||
@proxy = new DraftStoreProxy('client-id', @draft)
|
||||
|
||||
spyOn(DatabaseTransaction.prototype, "persistModel").andReturn Promise.resolve()
|
||||
spyOn(DatabaseTransaction.prototype, "_query").andReturn Promise.resolve()
|
||||
spyOn(Actions, "queueTask").andReturn Promise.resolve()
|
||||
|
||||
it "should ignore the update unless it applies to the current draft", ->
|
||||
|
|
|
@ -37,7 +37,6 @@ class TestExtension extends ComposerExtension
|
|||
|
||||
describe "DraftStore", ->
|
||||
beforeEach ->
|
||||
spyOn(DatabaseTransaction.prototype, '_query').andCallFake -> Promise.resolve([])
|
||||
spyOn(NylasEnv, 'newWindow').andCallFake ->
|
||||
for id, session of DraftStore._draftSessions
|
||||
if session.teardown
|
||||
|
@ -671,6 +670,7 @@ describe "DraftStore", ->
|
|||
spyOn(DraftStore, "_doneWithSession").andCallThrough()
|
||||
spyOn(DraftStore, "trigger")
|
||||
spyOn(SoundRegistry, "playSound")
|
||||
spyOn(Actions, "queueTask")
|
||||
|
||||
it "plays a sound immediately when sending draft", ->
|
||||
spyOn(NylasEnv.config, "get").andReturn true
|
||||
|
@ -686,20 +686,14 @@ describe "DraftStore", ->
|
|||
|
||||
it "sets the sending state when sending", ->
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn true
|
||||
spyOn(Actions, "queueTask").andCallThrough()
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
Actions.queueTask.calls.length > 0
|
||||
runs ->
|
||||
expect(DraftStore.isSendingDraft(draftClientId)).toBe true
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
expect(DraftStore.isSendingDraft(draftClientId)).toBe true
|
||||
|
||||
# Since all changes haven't been applied yet, we want to ensure that
|
||||
# no view of the draft renders the draft as if its sending, but with
|
||||
# the wrong text.
|
||||
it "does NOT trigger until the latest changes have been applied", ->
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn true
|
||||
spyOn(Actions, "queueTask").andCallThrough()
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
expect(DraftStore.trigger).not.toHaveBeenCalled()
|
||||
|
@ -743,7 +737,6 @@ describe "DraftStore", ->
|
|||
expect(NylasEnv.close).not.toHaveBeenCalled()
|
||||
|
||||
it "forces a commit to happen before sending", ->
|
||||
spyOn(Actions, "queueTask")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
|
@ -752,7 +745,6 @@ describe "DraftStore", ->
|
|||
expect(@forceCommit).toBe true
|
||||
|
||||
it "queues a SendDraftTask", ->
|
||||
spyOn(Actions, "queueTask")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
|
@ -768,7 +760,6 @@ describe "DraftStore", ->
|
|||
spyOn(NylasEnv, "getWindowType").andReturn "composer"
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn false
|
||||
spyOn(NylasEnv, "close")
|
||||
spyOn(Actions, "queueTask")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
|
|
|
@ -129,12 +129,14 @@ describe "ThreadCountsStore", ->
|
|||
|
||||
describe "when a count fails", ->
|
||||
it "should not immediately try to count any other categories", ->
|
||||
spyOn(console, "warn")
|
||||
ThreadCountsStore._fetchCountsMissing()
|
||||
spyOn(ThreadCountsStore, '_fetchCountsMissing')
|
||||
spyOn(console, 'error')
|
||||
advanceClock()
|
||||
@countReject(new Error("Oh man something really bad."))
|
||||
advanceClock()
|
||||
expect(console.warn).toHaveBeenCalled()
|
||||
expect(ThreadCountsStore._fetchCountsMissing).not.toHaveBeenCalled()
|
||||
|
||||
describe "_fetchCountForCategory", ->
|
||||
|
@ -174,7 +176,6 @@ describe "ThreadCountsStore", ->
|
|||
})
|
||||
|
||||
it "should persist the new counts to the database", ->
|
||||
spyOn(DatabaseStore, '_query').andCallFake -> Promise.resolve([])
|
||||
spyOn(DatabaseTransaction.prototype, 'persistJSONBlob')
|
||||
runs =>
|
||||
ThreadCountsStore._saveCounts()
|
||||
|
|
|
@ -50,7 +50,6 @@ describe "ChangeMailTask", ->
|
|||
|
||||
spyOn(DatabaseTransaction.prototype, 'persistModels').andReturn(Promise.resolve())
|
||||
spyOn(DatabaseTransaction.prototype, 'persistModel').andReturn(Promise.resolve())
|
||||
spyOn(DatabaseTransaction.prototype, '_query').andReturn(Promise.resolve([]))
|
||||
|
||||
it "leaves subclasses to implement changesToModel", ->
|
||||
task = new ChangeMailTask()
|
||||
|
|
169
spec/tasks/create-model-task-spec.es6
Normal file
169
spec/tasks/create-model-task-spec.es6
Normal file
|
@ -0,0 +1,169 @@
|
|||
import {
|
||||
Task,
|
||||
NylasAPI,
|
||||
APIError,
|
||||
CreateModelTask,
|
||||
DatabaseTransaction } from 'nylas-exports'
|
||||
|
||||
describe("CreateModelTask", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(DatabaseTransaction.prototype, "persistModel")
|
||||
});
|
||||
|
||||
it("constructs without error", () => {
|
||||
const t = new CreateModelTask()
|
||||
expect(t._rememberedToCallSuper).toBe(true)
|
||||
});
|
||||
|
||||
describe("performLocal", () => {
|
||||
it("throws if basic fields are missing", () => {
|
||||
const t = new CreateModelTask()
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Must pass.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("throws if `requiredFields` are missing", () => {
|
||||
const accountId = "a123"
|
||||
const modelName = "Metadata"
|
||||
const endpoint = "/endpoint"
|
||||
const data = {foo: "bar"}
|
||||
const requiredFields = ["stuff"]
|
||||
const t = new CreateModelTask({accountId, modelName, data, endpoint, requiredFields})
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Must pass data field.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("throws if the model name can't be found", () => {
|
||||
const accountId = "a123"
|
||||
const modelName = "dne"
|
||||
const endpoint = "/endpoint"
|
||||
const data = {stuff: "bar"}
|
||||
const requiredFields = ["stuff"]
|
||||
const t = new CreateModelTask({accountId, modelName, data, endpoint, requiredFields})
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Couldn't find the class for.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("persists the new model properly", () => {
|
||||
const persistFn = DatabaseTransaction.prototype.persistModel
|
||||
const accountId = "a123"
|
||||
const modelName = "Metadata"
|
||||
const endpoint = "/endpoint"
|
||||
const data = {key: "foo", value: "bar"}
|
||||
const requiredFields = ["key", "value"]
|
||||
const t = new CreateModelTask({accountId, modelName, data, endpoint, requiredFields})
|
||||
window.waitsForPromise(() => {
|
||||
return t.performLocal().then(() => {
|
||||
expect(persistFn).toHaveBeenCalled()
|
||||
const model = persistFn.calls[0].args[0]
|
||||
expect(model.constructor.name).toBe(modelName)
|
||||
expect(model.key).toBe("foo")
|
||||
expect(model.value).toBe("bar")
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("performRemote", () => {
|
||||
const accountId = "a123"
|
||||
const modelName = "Metadata"
|
||||
const endpoint = "/endpoint"
|
||||
const data = {key: "foo", value: "bar"}
|
||||
|
||||
beforeEach(() => {
|
||||
this.task = new CreateModelTask({accountId, modelName, data, endpoint})
|
||||
});
|
||||
|
||||
const performRemote = (fn) => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
return this.task.performRemote().then(fn)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("makes a POST request to the Nylas API", () => {
|
||||
spyOn(NylasAPI, "makeRequest").andReturn(Promise.resolve())
|
||||
performRemote(() => {
|
||||
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
||||
expect(opts.method).toBe("POST")
|
||||
expect(opts.body.key).toBe("foo")
|
||||
})
|
||||
});
|
||||
|
||||
it("marks task success on API success", () => {
|
||||
spyOn(NylasAPI, "makeRequest").andReturn(Promise.resolve())
|
||||
performRemote((status) => {
|
||||
expect(status).toBe(Task.Status.Success)
|
||||
})
|
||||
});
|
||||
|
||||
it("retries on non permanent errors", () => {
|
||||
spyOn(NylasAPI, "makeRequest").andCallFake(() => {
|
||||
return Promise.reject(new APIError({statusCode: 429}))
|
||||
})
|
||||
performRemote((status) => {
|
||||
expect(status).toBe(Task.Status.Retry)
|
||||
})
|
||||
});
|
||||
|
||||
it("fails on permanent errors", () => {
|
||||
const err = new APIError({statusCode: 500})
|
||||
spyOn(NylasAPI, "makeRequest").andCallFake(() => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
performRemote((status) => {
|
||||
expect(status).toEqual([Task.Status.Failed, err])
|
||||
})
|
||||
});
|
||||
|
||||
it("fails on other thrown errors", () => {
|
||||
const err = new Error("foo")
|
||||
spyOn(NylasAPI, "makeRequest").andCallFake(() => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
performRemote((status) => {
|
||||
expect(status).toEqual([Task.Status.Failed, err])
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe("undo", () => {
|
||||
const accountId = "a123"
|
||||
const modelName = "Metadata"
|
||||
const endpoint = "/endpoint"
|
||||
const data = {key: "foo", value: "bar"}
|
||||
|
||||
beforeEach(() => {
|
||||
this.task = new CreateModelTask({accountId, modelName, data, endpoint})
|
||||
});
|
||||
|
||||
it("indicates it's undoable", () => {
|
||||
expect(this.task.canBeUndone()).toBe(true)
|
||||
expect(this.task.isUndo()).toBe(false)
|
||||
});
|
||||
|
||||
it("creates the appropriate DestroyModelTask", () => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
const undoTask = this.task.createUndoTask()
|
||||
expect(undoTask.constructor.name).toBe("DestroyModelTask")
|
||||
expect(undoTask.clientId).toBe(this.task.model.clientId)
|
||||
expect(undoTask.isUndo()).toBe(true)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,7 +29,6 @@ describe "DestroyCategoryTask", ->
|
|||
category: category
|
||||
|
||||
beforeEach ->
|
||||
spyOn(DatabaseTransaction.prototype, '_query').andCallFake -> Promise.resolve([])
|
||||
spyOn(DatabaseTransaction.prototype, 'persistModel').andCallFake -> Promise.resolve()
|
||||
|
||||
describe "performLocal", ->
|
||||
|
|
140
spec/tasks/destroy-model-task-spec.es6
Normal file
140
spec/tasks/destroy-model-task-spec.es6
Normal file
|
@ -0,0 +1,140 @@
|
|||
import {
|
||||
Metadata,
|
||||
NylasAPI,
|
||||
DatabaseStore,
|
||||
DestroyModelTask,
|
||||
DatabaseTransaction} from 'nylas-exports'
|
||||
|
||||
describe("DestroyModelTask", () => {
|
||||
beforeEach(() => {
|
||||
this.existingModel = new Metadata({key: "foo", value: "bar"})
|
||||
this.existingModel.clientId = "local-123"
|
||||
this.existingModel.serverId = "server-123"
|
||||
spyOn(DatabaseTransaction.prototype, "unpersistModel")
|
||||
spyOn(DatabaseStore, "findBy").andCallFake(() => {
|
||||
return Promise.resolve(this.existingModel)
|
||||
})
|
||||
|
||||
this.defaultArgs = {
|
||||
clientId: "local-123",
|
||||
accountId: "a123",
|
||||
modelName: "Metadata",
|
||||
endpoint: "/endpoint",
|
||||
}
|
||||
});
|
||||
|
||||
it("constructs without error", () => {
|
||||
const t = new DestroyModelTask()
|
||||
expect(t._rememberedToCallSuper).toBe(true)
|
||||
});
|
||||
|
||||
describe("performLocal", () => {
|
||||
it("throws if basic fields are missing", () => {
|
||||
const t = new DestroyModelTask()
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Must pass.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("throws if the model name can't be found", () => {
|
||||
this.defaultArgs.modelName = "dne"
|
||||
const t = new DestroyModelTask(this.defaultArgs)
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Couldn't find the class for.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("throws if it can't find the object", () => {
|
||||
jasmine.unspy(DatabaseStore, "findBy")
|
||||
spyOn(DatabaseStore, "findBy").andCallFake(() => {
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
const t = new DestroyModelTask(this.defaultArgs)
|
||||
window.waitsForPromise(() => {
|
||||
return t.performLocal().then(() => {
|
||||
throw new Error("Shouldn't succeed")
|
||||
}).catch((err) => {
|
||||
expect(err.message).toMatch(/^Couldn't find the model with clientId.*/)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("unpersists the new existing model properly", () => {
|
||||
const unpersistFn = DatabaseTransaction.prototype.unpersistModel
|
||||
const t = new DestroyModelTask(this.defaultArgs)
|
||||
window.waitsForPromise(() => {
|
||||
return t.performLocal().then(() => {
|
||||
expect(unpersistFn).toHaveBeenCalled()
|
||||
const model = unpersistFn.calls[0].args[0]
|
||||
expect(model).toBe(this.existingModel)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("performRemote", () => {
|
||||
beforeEach(() => {
|
||||
this.task = new DestroyModelTask(this.defaultArgs)
|
||||
});
|
||||
|
||||
const performRemote = (fn) => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
return this.task.performRemote().then(fn)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("throws an error if the serverId is undefined", () => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
this.task.serverId = null
|
||||
try {
|
||||
this.task.performRemote()
|
||||
throw new Error("Should fail")
|
||||
} catch (err) {
|
||||
expect(err.message).toMatch(/^Need a serverId.*/)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("makes a DELETE request to the Nylas API", () => {
|
||||
spyOn(NylasAPI, "makeRequest").andReturn(Promise.resolve())
|
||||
performRemote(() => {
|
||||
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
||||
expect(opts.method).toBe("DELETE")
|
||||
expect(opts.path).toBe("/endpoint/server-123")
|
||||
expect(opts.accountId).toBe(this.defaultArgs.accountId)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe("undo", () => {
|
||||
beforeEach(() => {
|
||||
this.task = new DestroyModelTask(this.defaultArgs)
|
||||
});
|
||||
|
||||
it("indicates it's undoable", () => {
|
||||
expect(this.task.canBeUndone()).toBe(true)
|
||||
expect(this.task.isUndo()).toBe(false)
|
||||
});
|
||||
|
||||
it("creates the appropriate CreateModelTask", () => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
const undoTask = this.task.createUndoTask()
|
||||
expect(undoTask.constructor.name).toBe("CreateModelTask")
|
||||
expect(undoTask.data).toBe(this.task.oldModel)
|
||||
expect(undoTask.isUndo()).toBe(true)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -12,7 +12,6 @@ _ = require 'underscore'
|
|||
describe "EventRSVPTask", ->
|
||||
beforeEach ->
|
||||
spyOn(DatabaseStore, 'find').andCallFake => Promise.resolve(@event)
|
||||
spyOn(DatabaseTransaction.prototype, '_query').andCallFake -> Promise.resolve([])
|
||||
spyOn(DatabaseTransaction.prototype, 'persistModel').andCallFake -> Promise.resolve()
|
||||
@myName = "Ben Tester"
|
||||
@myEmail = "tester@nylas.com"
|
||||
|
|
|
@ -15,16 +15,6 @@ _ = require 'underscore'
|
|||
DBt = DatabaseTransaction.prototype
|
||||
|
||||
describe "SendDraftTask", ->
|
||||
beforeEach ->
|
||||
## TODO FIXME: If we don't spy on DatabaseStore._query, then
|
||||
# `DatabaseStore.inTransaction` will never complete and cause all
|
||||
# tests that depend on transactions to hang.
|
||||
#
|
||||
# @_query("BEGIN IMMEDIATE TRANSACTION") never resolves because
|
||||
# DatabaseStore._query never runs because the @_open flag is always
|
||||
# false because we never setup the DB when `NylasEnv.inSpecMode` is
|
||||
# true.
|
||||
spyOn(DatabaseStore, '_query').andCallFake => Promise.resolve([])
|
||||
|
||||
describe "isDependentTask", ->
|
||||
it "is not dependent on any pending SyncbackDraftTasks", ->
|
||||
|
|
|
@ -27,7 +27,6 @@ describe "SyncbackCategoryTask", ->
|
|||
beforeEach ->
|
||||
spyOn(NylasAPI, "makeRequest").andCallFake ->
|
||||
Promise.resolve(id: "server-444")
|
||||
spyOn(DatabaseTransaction.prototype, "_query").andCallFake => Promise.resolve([])
|
||||
spyOn(DatabaseTransaction.prototype, "persistModel")
|
||||
|
||||
it "sends API req to /labels if user uses labels", ->
|
||||
|
|
|
@ -41,8 +41,6 @@ describe "SyncbackDraftTask", ->
|
|||
else if clientId is "missingDraftId" then Promise.resolve()
|
||||
else return Promise.resolve()
|
||||
|
||||
spyOn(DatabaseTransaction.prototype, "_query").andCallFake ->
|
||||
Promise.resolve([])
|
||||
spyOn(DatabaseTransaction.prototype, "persistModel").andCallFake ->
|
||||
Promise.resolve()
|
||||
|
||||
|
|
143
spec/tasks/update-model-task-spec.es6
Normal file
143
spec/tasks/update-model-task-spec.es6
Normal file
|
@ -0,0 +1,143 @@
|
|||
import {
|
||||
Metadata,
|
||||
NylasAPI,
|
||||
DatabaseStore,
|
||||
UpdateModelTask,
|
||||
DatabaseTransaction} from 'nylas-exports'
|
||||
|
||||
describe("UpdateModelTask", () => {
|
||||
beforeEach(() => {
|
||||
this.existingModel = new Metadata({key: "foo", value: "bar"})
|
||||
this.existingModel.clientId = "local-123"
|
||||
this.existingModel.serverId = "server-123"
|
||||
spyOn(DatabaseTransaction.prototype, "persistModel")
|
||||
spyOn(DatabaseStore, "findBy").andCallFake(() => {
|
||||
return Promise.resolve(this.existingModel)
|
||||
})
|
||||
|
||||
this.defaultArgs = {
|
||||
clientId: "local-123",
|
||||
newData: {value: "baz"},
|
||||
accountId: "a123",
|
||||
modelName: "Metadata",
|
||||
endpoint: "/endpoint",
|
||||
}
|
||||
});
|
||||
|
||||
it("constructs without error", () => {
|
||||
const t = new UpdateModelTask()
|
||||
expect(t._rememberedToCallSuper).toBe(true)
|
||||
});
|
||||
|
||||
describe("performLocal", () => {
|
||||
it("throws if basic fields are missing", () => {
|
||||
const t = new UpdateModelTask()
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Must pass.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("throws if the model name can't be found", () => {
|
||||
this.defaultArgs.modelName = "dne"
|
||||
const t = new UpdateModelTask(this.defaultArgs)
|
||||
try {
|
||||
t.performLocal()
|
||||
throw new Error("Shouldn't succeed");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/^Couldn't find the class for.*/)
|
||||
}
|
||||
});
|
||||
|
||||
it("throws if it can't find the object", () => {
|
||||
jasmine.unspy(DatabaseStore, "findBy")
|
||||
spyOn(DatabaseStore, "findBy").andCallFake(() => {
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
const t = new UpdateModelTask(this.defaultArgs)
|
||||
window.waitsForPromise(() => {
|
||||
return t.performLocal().then(() => {
|
||||
throw new Error("Shouldn't succeed")
|
||||
}).catch((err) => {
|
||||
expect(err.message).toMatch(/^Couldn't find the model with clientId.*/)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("persists the new existing model properly", () => {
|
||||
const persistFn = DatabaseTransaction.prototype.persistModel
|
||||
const t = new UpdateModelTask(this.defaultArgs)
|
||||
window.waitsForPromise(() => {
|
||||
return t.performLocal().then(() => {
|
||||
expect(persistFn).toHaveBeenCalled()
|
||||
const model = persistFn.calls[0].args[0]
|
||||
expect(model).toBe(this.existingModel)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("performRemote", () => {
|
||||
beforeEach(() => {
|
||||
this.task = new UpdateModelTask(this.defaultArgs)
|
||||
});
|
||||
|
||||
const performRemote = (fn) => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
return this.task.performRemote().then(fn)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("throws an error if the serverId is undefined", () => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
this.task.serverId = null
|
||||
try {
|
||||
this.task.performRemote()
|
||||
throw new Error("Should fail")
|
||||
} catch (err) {
|
||||
expect(err.message).toMatch(/^Need a serverId.*/)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("makes a PUT request to the Nylas API", () => {
|
||||
spyOn(NylasAPI, "makeRequest").andReturn(Promise.resolve())
|
||||
performRemote(() => {
|
||||
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
||||
expect(opts.method).toBe("PUT")
|
||||
expect(opts.path).toBe("/endpoint/server-123")
|
||||
expect(opts.body).toBe(this.defaultArgs.newData)
|
||||
expect(opts.accountId).toBe(this.defaultArgs.accountId)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe("undo", () => {
|
||||
beforeEach(() => {
|
||||
this.task = new UpdateModelTask(this.defaultArgs)
|
||||
});
|
||||
|
||||
it("indicates it's undoable", () => {
|
||||
expect(this.task.canBeUndone()).toBe(true)
|
||||
expect(this.task.isUndo()).toBe(false)
|
||||
});
|
||||
|
||||
it("creates the appropriate UpdateModelTask", () => {
|
||||
window.waitsForPromise(() => {
|
||||
return this.task.performLocal().then(() => {
|
||||
const undoTask = this.task.createUndoTask()
|
||||
expect(undoTask.constructor.name).toBe("UpdateModelTask")
|
||||
expect(undoTask.newData).toBe(this.task.oldModel)
|
||||
expect(undoTask.newData.value).toBe("bar")
|
||||
expect(undoTask.isUndo()).toBe(true)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,6 +17,14 @@ describe "ThemeManager", ->
|
|||
spyOn(console, "warn")
|
||||
spyOn(console, "error")
|
||||
theme_dir = path.resolve(__dirname, '../internal_packages')
|
||||
|
||||
# Don't load ALL of our packages. Some packages may do very expensive
|
||||
# and asynchronous things on require, including hitting the database.
|
||||
packagePaths = [
|
||||
path.resolve(__dirname, '../internal_packages/ui-light')
|
||||
path.resolve(__dirname, '../internal_packages/ui-dark')
|
||||
]
|
||||
spyOn(NylasEnv.packages, "getAvailablePackagePaths").andReturn packagePaths
|
||||
NylasEnv.packages.packageDirPaths.unshift(theme_dir)
|
||||
themeManager = new ThemeManager({packageManager: NylasEnv.packages, resourcePath, configDirPath})
|
||||
|
||||
|
@ -33,7 +41,7 @@ describe "ThemeManager", ->
|
|||
|
||||
it 'getLoadedThemes get all the loaded themes', ->
|
||||
themes = themeManager.getLoadedThemes()
|
||||
expect(themes.length).toBeGreaterThan(2)
|
||||
expect(themes.length).toEqual(2)
|
||||
|
||||
it 'getActiveThemes get all the active themes', ->
|
||||
waitsForPromise ->
|
||||
|
|
|
@ -500,10 +500,6 @@ class Actions
|
|||
###
|
||||
@pushSheet: ActionScopeWindow
|
||||
|
||||
@metadataError: ActionScopeWindow
|
||||
@metadataCreated: ActionScopeWindow
|
||||
@metadataDestroyed: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Publish a user event to any analytics services linked to N1.
|
||||
###
|
||||
|
|
|
@ -121,6 +121,8 @@ class ThreadCountsStore extends NylasStore
|
|||
# but we don't want to flood the db with expensive SELECT COUNT queries.
|
||||
_.delay(@_fetchCountsMissing, 3000)
|
||||
@_saveCountsSoon()
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
|
||||
# This method is not intended to return a promise and it
|
||||
# could cause strange chaining.
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
Task = require './task'
|
||||
{APIError} = require '../errors'
|
||||
Actions = require '../actions'
|
||||
Metadata = require '../models/metadata'
|
||||
EdgehillAPI = require '../edgehill-api'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
AccountStore = require '../stores/account-store'
|
||||
|
||||
module.exports =
|
||||
class CreateMetadataTask extends Task
|
||||
constructor: ({@type, @publicId, @key, @value}) ->
|
||||
super
|
||||
@name = "CreateMetadataTask"
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
@_isSameTask(other) or @_isOldDestroyTask(other)
|
||||
|
||||
_isSameTask: (other) ->
|
||||
other instanceof CreateMetadataTask and
|
||||
@type is other.type and @publicId is other.publicId and
|
||||
@key is other.key and @value is other.value
|
||||
|
||||
_isOldDestroyTask: (other) ->
|
||||
other.name is "DestroyMetadataTask"
|
||||
@type is other.type and @publicId is other.publicId and @key is other.key
|
||||
|
||||
performLocal: ->
|
||||
return Promise.reject(new Error("Must pass a type")) unless @type?
|
||||
@metadatum = new Metadata({@type, @publicId, @key, @value})
|
||||
DatabaseStore.inTransaction (t) =>
|
||||
t.persistModel(@metadatum)
|
||||
|
||||
performRemote: ->
|
||||
new Promise (resolve, reject) =>
|
||||
EdgehillAPI.request
|
||||
method: "POST"
|
||||
path: "/metadata/#{AccountStore.current().id}/#{@type}/#{@publicId}"
|
||||
body:
|
||||
key: @key
|
||||
value: @value
|
||||
success: =>
|
||||
Actions.metadataCreated @type, @metadatum
|
||||
resolve(Task.Status.Success)
|
||||
error: (apiError) =>
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "APIError"
|
||||
error: apiError
|
||||
resolve(Task.Status.Failed)
|
||||
|
||||
_baseErrorData: ->
|
||||
action: "create"
|
||||
className: @constructor.name
|
||||
type: @type
|
||||
publicId: @publicId
|
||||
key: @key
|
||||
value: @value
|
76
src/flux/tasks/create-model-task.es6
Normal file
76
src/flux/tasks/create-model-task.es6
Normal file
|
@ -0,0 +1,76 @@
|
|||
import _ from 'underscore'
|
||||
import Task from './task'
|
||||
import NylasAPI from '../nylas-api'
|
||||
import DatabaseStore from '../stores/database-store'
|
||||
|
||||
export default class CreateModelTask extends Task {
|
||||
|
||||
constructor({data = {}, modelName, endpoint, requiredFields = [], accountId} = {}) {
|
||||
super()
|
||||
this.data = data
|
||||
this.endpoint = endpoint
|
||||
this.modelName = modelName
|
||||
this.accountId = accountId
|
||||
this.requiredFields = requiredFields || []
|
||||
}
|
||||
|
||||
shouldDequeueOtherTask(other) {
|
||||
return (other instanceof CreateModelTask &&
|
||||
this.modelName === other.modelName &&
|
||||
this.accountId === other.accountId &&
|
||||
this.endpoint === other.endpoint &&
|
||||
_.isEqual(this.data, other.data))
|
||||
}
|
||||
|
||||
getModelConstructor() {
|
||||
return require('nylas-exports')[this.modelName]
|
||||
}
|
||||
|
||||
performLocal() {
|
||||
this.validateRequiredFields(["accountId", "endpoint"])
|
||||
|
||||
for (const field of this.requiredFields) {
|
||||
if (this.data[field] === null || this.data[field] === undefined) {
|
||||
throw new Error(`Must pass data field "${field}"`)
|
||||
}
|
||||
}
|
||||
|
||||
const Klass = require('nylas-exports')[this.modelName]
|
||||
if (!_.isFunction(Klass)) {
|
||||
throw new Error(`Couldn't find the class for ${this.modelName}`)
|
||||
}
|
||||
|
||||
this.model = new Klass(this.data)
|
||||
return DatabaseStore.inTransaction((t) => {
|
||||
return t.persistModel(this.model)
|
||||
});
|
||||
}
|
||||
|
||||
performRemote() {
|
||||
return NylasAPI.makeRequest({
|
||||
path: this.endpoint,
|
||||
method: "POST",
|
||||
accountId: this.accountId,
|
||||
body: this.model.toJSON(),
|
||||
returnsModel: true,
|
||||
}).then(() => {
|
||||
return Promise.resolve(Task.Status.Success)
|
||||
}).catch(this.apiErrorHandler)
|
||||
}
|
||||
|
||||
canBeUndone() { return true }
|
||||
|
||||
isUndo() { return !!this._isUndoTask }
|
||||
|
||||
createUndoTask() {
|
||||
const DestroyModelTask = require('./destroy-model-task')
|
||||
const undoTask = new DestroyModelTask({
|
||||
clientId: this.model.clientId,
|
||||
modelName: this.modelName,
|
||||
endpoint: this.endpoint,
|
||||
accountId: this.accountId,
|
||||
})
|
||||
undoTask._isUndoTask = true
|
||||
return undoTask
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
Task = require './task'
|
||||
{APIError} = require '../errors'
|
||||
Actions = require '../actions'
|
||||
Metadata = require '../models/metadata'
|
||||
EdgehillAPI = require '../edgehill-api'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
AccountStore = require '../stores/account-store'
|
||||
|
||||
module.exports =
|
||||
class DestroyMetadataTask extends Task
|
||||
constructor: ({@type, @publicId, @key}) ->
|
||||
super
|
||||
@name = "DestroyMetadataTask"
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
@_isSameTask(other) or @_isOldCreateTask(other)
|
||||
|
||||
_isSameTask: (other) ->
|
||||
other instanceof DestroyMetadataTask and
|
||||
@type is other.type and @publicId is other.publicId and @key is other.key
|
||||
|
||||
_isOldCreateTask: (other) ->
|
||||
other.name is "CreateMetadataTask"
|
||||
@type is other.type and @publicId is other.publicId and @key is other.key
|
||||
|
||||
performLocal: ->
|
||||
return Promise.reject(new Error("Must pass a type")) unless @type?
|
||||
return Promise.reject(new Error("Must pass an publicId")) unless @publicId?
|
||||
new Promise (resolve, reject) =>
|
||||
if @key?
|
||||
matcher = {@type, @publicId, @key}
|
||||
else
|
||||
matcher = {@type, @publicId}
|
||||
|
||||
DatabaseStore.findAll(Metadata, matcher)
|
||||
.then (models) ->
|
||||
if (models ? []).length is 0
|
||||
resolve()
|
||||
else
|
||||
DatabaseStore.inTransaction (t) ->
|
||||
promises = models.map (m) ->
|
||||
t.unpersistModel(m)
|
||||
Promise.settle(promises)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.catch (error) ->
|
||||
console.error "Error finding Metadata to destroy", error
|
||||
console.error error.stack
|
||||
reject(error)
|
||||
|
||||
performRemote: -> new Promise (resolve, reject) =>
|
||||
if @key?
|
||||
body = {@key}
|
||||
else
|
||||
body = null
|
||||
|
||||
EdgehillAPI.request
|
||||
method: "DELETE"
|
||||
path: "/metadata/#{AccountStore.current().id}/#{@type}/#{@publicId}"
|
||||
body: body
|
||||
success: =>
|
||||
Actions.metadataDestroyed(@type)
|
||||
resolve(Task.Status.Success)
|
||||
error: (apiError) =>
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "APIError"
|
||||
error: apiError
|
||||
resolve(Task.Status.Failed)
|
||||
|
||||
_baseErrorData: ->
|
||||
action: "destroy"
|
||||
className: @constructor.name
|
||||
type: @type
|
||||
publicId: @publicId
|
||||
key: @key
|
76
src/flux/tasks/destroy-model-task.es6
Normal file
76
src/flux/tasks/destroy-model-task.es6
Normal file
|
@ -0,0 +1,76 @@
|
|||
import _ from 'underscore'
|
||||
import Task from './task'
|
||||
import NylasAPI from '../nylas-api'
|
||||
import DatabaseStore from '../stores/database-store'
|
||||
|
||||
export default class DestroyModelTask extends Task {
|
||||
|
||||
constructor({clientId, modelName, endpoint, accountId} = {}) {
|
||||
super()
|
||||
this.clientId = clientId
|
||||
this.endpoint = endpoint
|
||||
this.modelName = modelName
|
||||
this.accountId = accountId
|
||||
}
|
||||
|
||||
shouldDequeueOtherTask(other) {
|
||||
return (other instanceof DestroyModelTask &&
|
||||
this.modelName === other.modelName &&
|
||||
this.accountId === other.accountId &&
|
||||
this.endpoint === other.endpoint &&
|
||||
this.clientId === other.clientId)
|
||||
}
|
||||
|
||||
getModelConstructor() {
|
||||
return require('nylas-exports')[this.modelName]
|
||||
}
|
||||
|
||||
performLocal() {
|
||||
this.validateRequiredFields(["clientId", "accountId", "endpoint"])
|
||||
|
||||
const klass = this.getModelConstructor()
|
||||
if (!_.isFunction(klass)) {
|
||||
throw new Error(`Couldn't find the class for ${this.modelName}`)
|
||||
}
|
||||
|
||||
return DatabaseStore.findBy(klass, {clientId: this.clientId}).then((model) => {
|
||||
if (!model) {
|
||||
throw new Error(`Couldn't find the model with clientId ${this.clientId}`)
|
||||
}
|
||||
this.serverId = model.serverId
|
||||
this.oldModel = model.clone()
|
||||
return DatabaseStore.inTransaction((t) => {
|
||||
return t.unpersistModel(model)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
performRemote() {
|
||||
if (!this.serverId) {
|
||||
throw new Error("Need a serverId to destroy remotely")
|
||||
}
|
||||
return NylasAPI.makeRequest({
|
||||
path: `${this.endpoint}/${this.serverId}`,
|
||||
method: "DELETE",
|
||||
accountId: this.accountId,
|
||||
}).then(() => {
|
||||
return Promise.resolve(Task.Status.Success)
|
||||
}).catch(this.apiErrorHandler)
|
||||
}
|
||||
|
||||
canBeUndone() { return true }
|
||||
|
||||
isUndo() { return !!this._isUndoTask }
|
||||
|
||||
createUndoTask() {
|
||||
const CreateModelTask = require('./create-model-task')
|
||||
const undoTask = new CreateModelTask({
|
||||
data: this.oldModel,
|
||||
modelName: this.modelName,
|
||||
endpoint: this.endpoint,
|
||||
accountId: this.accountId,
|
||||
})
|
||||
undoTask._isUndoTask = true
|
||||
return undoTask
|
||||
}
|
||||
}
|
|
@ -269,6 +269,29 @@ class Task
|
|||
|
||||
return Promise.reject(err)
|
||||
|
||||
########################################################################
|
||||
########################## HELPER METHODS ##############################
|
||||
########################################################################
|
||||
|
||||
validateRequiredFields: (fields=[]) =>
|
||||
for field in fields
|
||||
if not this[field]? then throw new Error("Must pass #{field}")
|
||||
|
||||
# Most tasks that interact with a RESTful API will want to behave in a
|
||||
# similar way. We retry on temproary API error codes and permenantly
|
||||
# fail on others.
|
||||
apiErrorHandler: (err={}) =>
|
||||
{PermanentErrorCodes} = require '../nylas-api'
|
||||
{APIError} = require '../errors'
|
||||
|
||||
if err instanceof APIError
|
||||
if err.statusCode in PermanentErrorCodes
|
||||
return Promise.resolve([Task.Status.Failed, err])
|
||||
else
|
||||
return Promise.resolve(Task.Status.Retry)
|
||||
else
|
||||
return Promise.resolve([Task.Status.Failed, err])
|
||||
|
||||
########################################################################
|
||||
######################## METHODS TO OVERRIDE ###########################
|
||||
########################################################################
|
||||
|
|
81
src/flux/tasks/update-model-task.es6
Normal file
81
src/flux/tasks/update-model-task.es6
Normal file
|
@ -0,0 +1,81 @@
|
|||
import _ from 'underscore'
|
||||
import Task from './task'
|
||||
import NylasAPI from '../nylas-api'
|
||||
import DatabaseStore from '../stores/database-store'
|
||||
|
||||
export default class UpdateModelTask extends Task {
|
||||
|
||||
constructor({clientId, newData = {}, modelName, endpoint, accountId} = {}) {
|
||||
super()
|
||||
this.clientId = clientId
|
||||
this.newData = newData
|
||||
this.endpoint = endpoint
|
||||
this.modelName = modelName
|
||||
this.accountId = accountId
|
||||
}
|
||||
|
||||
shouldDequeueOtherTask(other) {
|
||||
return (other instanceof UpdateModelTask &&
|
||||
this.clientId === other.clientId &&
|
||||
this.modelName === other.modelName &&
|
||||
this.accountId === other.accountId &&
|
||||
this.endpoint === other.endpoint &&
|
||||
_.isEqual(this.newData, other.newData))
|
||||
}
|
||||
|
||||
getModelConstructor() {
|
||||
return require('nylas-exports')[this.modelName]
|
||||
}
|
||||
|
||||
performLocal() {
|
||||
this.validateRequiredFields(["clientId", "accountId", "endpoint"])
|
||||
|
||||
const klass = this.getModelConstructor()
|
||||
if (!_.isFunction(klass)) {
|
||||
throw new Error(`Couldn't find the class for ${this.modelName}`)
|
||||
}
|
||||
|
||||
return DatabaseStore.findBy(klass, {clientId: this.clientId}).then((model) => {
|
||||
if (!model) {
|
||||
throw new Error(`Couldn't find the model with clientId ${this.clientId}`)
|
||||
}
|
||||
this.serverId = model.serverId
|
||||
this.oldModel = model.clone()
|
||||
const updatedModel = _.extend(model, this.newData)
|
||||
return DatabaseStore.inTransaction((t) => {
|
||||
return t.persistModel(updatedModel)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
performRemote() {
|
||||
if (!this.serverId) {
|
||||
throw new Error("Need a serverId to update remotely")
|
||||
}
|
||||
return NylasAPI.makeRequest({
|
||||
path: `${this.endpoint}/${this.serverId}`,
|
||||
method: "PUT",
|
||||
accountId: this.accountId,
|
||||
body: this.newData,
|
||||
returnsModel: true,
|
||||
}).then(() => {
|
||||
return Promise.resolve(Task.Status.Success)
|
||||
}).catch(this.apiErrorHandler)
|
||||
}
|
||||
|
||||
canBeUndone() { return true }
|
||||
|
||||
isUndo() { return !!this._isUndoTask }
|
||||
|
||||
createUndoTask() {
|
||||
const undoTask = new UpdateModelTask({
|
||||
clientId: this.clientId,
|
||||
newData: this.oldModel,
|
||||
modelName: this.modelName,
|
||||
endpoint: this.endpoint,
|
||||
accountId: this.accountId,
|
||||
})
|
||||
undoTask._isUndoTask = true
|
||||
return undoTask
|
||||
}
|
||||
}
|
|
@ -103,8 +103,9 @@ class NylasExports
|
|||
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
||||
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft'
|
||||
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
||||
@require "CreateMetadataTask", 'flux/tasks/create-metadata-task'
|
||||
@require "DestroyMetadataTask", 'flux/tasks/destroy-metadata-task'
|
||||
@require "CreateModelTask", 'flux/tasks/create-model-task'
|
||||
@require "UpdateModelTask", 'flux/tasks/update-model-task'
|
||||
@require "DestroyModelTask", 'flux/tasks/destroy-model-task'
|
||||
@require "ReprocessMailRulesTask", 'flux/tasks/reprocess-mail-rules-task'
|
||||
|
||||
# Stores
|
||||
|
|
|
@ -246,9 +246,8 @@ class NylasEnvConstructor extends Model
|
|||
|
||||
@emitter.emit 'did-throw-error', {message, url, line, column, originalError}
|
||||
|
||||
# Since Bluebird is the promise library, we can properly report
|
||||
# unhandled errors from business logic inside promises.
|
||||
Promise.longStackTraces() unless @inSpecMode()
|
||||
if @inSpecMode() or @inDevMode()
|
||||
Promise.longStackTraces()
|
||||
|
||||
Promise.onPossiblyUnhandledRejection (error) =>
|
||||
error.stack = convertStackTrace(error.stack, sourceMapCache)
|
||||
|
@ -259,7 +258,7 @@ class NylasEnvConstructor extends Model
|
|||
return
|
||||
|
||||
if @inSpecMode()
|
||||
console.error(error.stack)
|
||||
jasmine.getEnv().currentSpec.fail(error)
|
||||
else if @inDevMode()
|
||||
console.error(error.message, error.stack, error)
|
||||
@openDevTools()
|
||||
|
|
Loading…
Add table
Reference in a new issue