Merge pull request #874 from nasa/persist-on-mutation-825

[Persistence] Persist on mutation
This commit is contained in:
Andrew Henry 2016-10-11 10:52:44 -07:00 committed by GitHub
commit f2d44114fa
35 changed files with 606 additions and 330 deletions

View File

@ -200,7 +200,6 @@ define([
"name": "Remove",
"description": "Remove this object from its containing object.",
"depends": [
"$q",
"navigationService"
]
},

View File

@ -40,19 +40,11 @@ define(
var self = this,
editAction = this.domainObject.getCapability('action').getActions("edit")[0];
// Persist changes to the domain object
function doPersist() {
var persistence =
self.domainObject.getCapability('persistence');
return persistence.persist();
}
// Link these objects
function doLink() {
var composition = self.domainObject &&
self.domainObject.getCapability('composition');
return composition && composition.add(self.selectedObject)
.then(doPersist);
return composition && composition.add(self.selectedObject);
}
if (editAction) {

View File

@ -50,12 +50,6 @@ define(
domainObject = this.domainObject,
dialogService = this.dialogService;
// Persist modifications to this domain object
function doPersist() {
var persistence = domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
// Update the domain object model based on user input
function updateModel(userInput, dialog) {
return domainObject.useCapability('mutation', function (model) {
@ -73,11 +67,9 @@ define(
dialog.getFormStructure(),
dialog.getInitialFormValue()
).then(function (userInput) {
// Update the model, if user input was provided
return userInput && updateModel(userInput, dialog);
}).then(function (result) {
return result && doPersist();
});
// Update the model, if user input was provided
return userInput && updateModel(userInput, dialog);
});
}
return type && showDialog(type);
@ -94,9 +86,7 @@ define(
creatable = type && type.hasFeature('creation');
// Only allow creatable types to be edited
return domainObject &&
domainObject.hasCapability("persistence") &&
creatable;
return domainObject && creatable;
};
return PropertiesAction;

View File

@ -39,9 +39,8 @@ define(
* @constructor
* @implements {Action}
*/
function RemoveAction($q, navigationService, context) {
function RemoveAction(navigationService, context) {
this.domainObject = (context || {}).domainObject;
this.$q = $q;
this.navigationService = navigationService;
}
@ -51,8 +50,7 @@ define(
* fulfilled when the action has completed.
*/
RemoveAction.prototype.perform = function () {
var $q = this.$q,
navigationService = this.navigationService,
var navigationService = this.navigationService,
domainObject = this.domainObject;
/*
* Check whether an object ID matches the ID of the object being
@ -71,15 +69,6 @@ define(
model.composition = model.composition.filter(isNotObject);
}
/*
* Invoke persistence on a domain object. This will be called upon
* the removed object's parent (as its composition will have changed.)
*/
function doPersist(domainObj) {
var persistence = domainObj.getCapability('persistence');
return persistence && persistence.persist();
}
/*
* Checks current object and ascendants of current
* object with object being removed, if the current
@ -119,15 +108,10 @@ define(
// navigates to existing object up tree
checkObjectNavigation(object, parent);
return $q.when(
parent.useCapability('mutation', doMutate)
).then(function () {
return doPersist(parent);
});
return parent.useCapability('mutation', doMutate);
}
return $q.when(domainObject)
.then(removeFromContext);
return removeFromContext(domainObject);
};
// Object needs to have a parent for Remove to be applicable

View File

@ -81,6 +81,10 @@ define(
return this.persistenceCapability.getSpace();
};
TransactionalPersistenceCapability.prototype.persisted = function () {
return this.persistenceCapability.persisted();
};
return TransactionalPersistenceCapability;
}
);

View File

@ -50,17 +50,13 @@ define(
this.listenHandle = undefined;
// Mutate and persist a new version of a domain object's model.
function doPersist(model) {
function doMutate(model) {
var domainObject = self.domainObject;
// First, mutate; then, persist.
return $q.when(domainObject.useCapability("mutation", function () {
return model;
})).then(function (result) {
// Only persist when mutation was successful
return result &&
domainObject.getCapability("persistence").persist();
});
}));
}
// Handle changes to model and/or view configuration
@ -80,14 +76,14 @@ define(
].join(" "));
// Update the configuration stored in the model, and persist.
if (domainObject && domainObject.hasCapability("persistence")) {
if (domainObject) {
// Configurations for specific views are stored by
// key in the "configuration" field of the model.
if (self.key && configuration) {
model.configuration = model.configuration || {};
model.configuration[self.key] = configuration;
}
doPersist(model);
doMutate(model);
}
}

View File

@ -0,0 +1,48 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./Transaction'], function (Transaction) {
/**
* A nested transaction is a transaction which takes place in the context
* of a larger parent transaction. It becomes part of the parent
* transaction when (and only when) committed.
* @param parent
* @constructor
* @extends {platform/commonUI/edit/services.Transaction}
* @memberof platform/commonUI/edit/services
*/
function NestedTransaction(parent) {
this.parent = parent;
Transaction.call(this, parent.$log);
}
NestedTransaction.prototype = Object.create(Transaction.prototype);
NestedTransaction.prototype.commit = function () {
this.parent.add(
Transaction.prototype.commit.bind(this),
Transaction.prototype.cancel.bind(this)
);
return Promise.resolve(true);
};
return NestedTransaction;
});

View File

@ -0,0 +1,96 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* A Transaction represents a set of changes that are intended to
* be kept or discarded as a unit.
* @param $log Angular's `$log` service, for logging messages
* @constructor
* @memberof platform/commonUI/edit/services
*/
function Transaction($log) {
this.$log = $log;
this.callbacks = [];
}
/**
* Add a change to the current transaction, as expressed by functions
* to either keep or discard the change.
* @param {Function} commit called when the transaction is committed
* @param {Function} cancel called when the transaction is cancelled
* @returns {Function) a function which may be called to remove this
* pair of callbacks from the transaction
*/
Transaction.prototype.add = function (commit, cancel) {
var callback = { commit: commit, cancel: cancel };
this.callbacks.push(callback);
return function () {
this.callbacks = this.callbacks.filter(function (c) {
return c !== callback;
});
}.bind(this);
};
/**
* Get the number of changes in the current transaction.
* @returns {number} the size of the current transaction
*/
Transaction.prototype.size = function () {
return this.callbacks.length;
};
/**
* Keep all changes associated with this transaction.
* @method {platform/commonUI/edit/services.Transaction#commit}
* @returns {Promise} a promise which will resolve when all callbacks
* have been handled.
*/
/**
* Discard all changes associated with this transaction.
* @method {platform/commonUI/edit/services.Transaction#cancel}
* @returns {Promise} a promise which will resolve when all callbacks
* have been handled.
*/
['commit', 'cancel'].forEach(function (method) {
Transaction.prototype[method] = function () {
var promises = [];
var callback;
while (this.callbacks.length > 0) {
callback = this.callbacks.shift();
try {
promises.push(callback[method]());
} catch (e) {
this.$log
.error("Error trying to " + method + " transaction.");
}
}
return Promise.all(promises);
};
});
return Transaction;
});

View File

@ -21,8 +21,8 @@
*****************************************************************************/
/*global define*/
define(
[],
function () {
['./Transaction', './NestedTransaction'],
function (Transaction, NestedTransaction) {
/**
* Implements an application-wide transaction state. Once a
* transaction is started, calls to
@ -37,10 +37,7 @@ define(
function TransactionService($q, $log) {
this.$q = $q;
this.$log = $log;
this.transaction = false;
this.onCommits = [];
this.onCancels = [];
this.transactions = [];
}
/**
@ -50,18 +47,18 @@ define(
* #cancel} are called
*/
TransactionService.prototype.startTransaction = function () {
if (this.transaction) {
//Log error because this is a programming error if it occurs.
this.$log.error("Transaction already in progress");
}
this.transaction = true;
var transaction = this.isActive() ?
new NestedTransaction(this.transactions[0]) :
new Transaction(this.$log);
this.transactions.push(transaction);
};
/**
* @returns {boolean} If true, indicates that a transaction is in progress
*/
TransactionService.prototype.isActive = function () {
return this.transaction;
return this.transactions.length > 0;
};
/**
@ -72,24 +69,20 @@ define(
* @param onCancel A function to call on cancel
*/
TransactionService.prototype.addToTransaction = function (onCommit, onCancel) {
if (this.transaction) {
this.onCommits.push(onCommit);
if (onCancel) {
this.onCancels.push(onCancel);
}
if (this.isActive()) {
return this.activeTransaction().add(onCommit, onCancel);
} else {
//Log error because this is a programming error if it occurs.
this.$log.error("No transaction in progress");
}
};
return function () {
this.onCommits = this.onCommits.filter(function (callback) {
return callback !== onCommit;
});
this.onCancels = this.onCancels.filter(function (callback) {
return callback !== onCancel;
});
}.bind(this);
/**
* Get the transaction at the top of the stack.
* @private
*/
TransactionService.prototype.activeTransaction = function () {
return this.transactions[this.transactions.length - 1];
};
/**
@ -100,24 +93,8 @@ define(
* completed. Will reject if any commit operations fail
*/
TransactionService.prototype.commit = function () {
var self = this,
promises = [],
onCommit;
while (this.onCommits.length > 0) { // ...using a while in case some onCommit adds to transaction
onCommit = this.onCommits.pop();
try { // ...also don't want to fail mid-loop...
promises.push(onCommit());
} catch (e) {
this.$log.error("Error committing transaction.");
}
}
return this.$q.all(promises).then(function () {
self.transaction = false;
self.onCommits = [];
self.onCancels = [];
});
var transaction = this.transactions.pop();
return transaction ? transaction.commit() : Promise.reject();
};
/**
@ -129,28 +106,17 @@ define(
* @returns {*}
*/
TransactionService.prototype.cancel = function () {
var self = this,
results = [],
onCancel;
while (this.onCancels.length > 0) {
onCancel = this.onCancels.pop();
try {
results.push(onCancel());
} catch (error) {
this.$log.error("Error cancelling transaction.");
}
}
return this.$q.all(results).then(function () {
self.transaction = false;
self.onCommits = [];
self.onCancels = [];
});
var transaction = this.transactions.pop();
return transaction ? transaction.cancel() : Promise.reject();
};
/**
* Get the size (the number of commit/cancel callbacks) of
* the active transaction.
* @returns {number} size of the active transaction
*/
TransactionService.prototype.size = function () {
return this.onCommits.length;
return this.isActive() ? this.activeTransaction().size() : 0;
};
return TransactionService;

View File

@ -30,7 +30,6 @@ define(
mockParent,
mockContext,
mockComposition,
mockPersistence,
mockActionCapability,
mockEditAction,
mockType,
@ -68,7 +67,6 @@ define(
};
mockContext = jasmine.createSpyObj("context", ["getParent"]);
mockComposition = jasmine.createSpyObj("composition", ["invoke", "add"]);
mockPersistence = jasmine.createSpyObj("persistence", ["persist"]);
mockType = jasmine.createSpyObj("type", ["hasFeature", "getKey"]);
mockActionCapability = jasmine.createSpyObj("actionCapability", ["getActions"]);
mockEditAction = jasmine.createSpyObj("editAction", ["perform"]);
@ -84,7 +82,6 @@ define(
capabilities = {
composition: mockComposition,
persistence: mockPersistence,
action: mockActionCapability,
type: mockType
};
@ -107,11 +104,6 @@ define(
.toHaveBeenCalledWith(mockDomainObject);
});
it("persists changes afterward", function () {
action.perform();
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("enables edit mode for objects that have an edit action", function () {
mockActionCapability.getActions.andReturn([mockEditAction]);
action.perform();

View File

@ -43,7 +43,6 @@ define(
},
hasFeature: jasmine.createSpy('hasFeature')
},
persistence: jasmine.createSpyObj("persistence", ["persist"]),
mutation: jasmine.createSpy("mutation")
};
model = {};
@ -78,25 +77,18 @@ define(
action = new PropertiesAction(dialogService, context);
});
it("persists when an action is performed", function () {
action.perform();
expect(capabilities.persistence.persist)
.toHaveBeenCalled();
});
it("does not persist any changes upon cancel", function () {
input = undefined;
action.perform();
expect(capabilities.persistence.persist)
.not.toHaveBeenCalled();
});
it("mutates an object when performed", function () {
action.perform();
expect(capabilities.mutation).toHaveBeenCalled();
capabilities.mutation.mostRecentCall.args[0]({});
});
it("does not muate object upon cancel", function () {
input = undefined;
action.perform();
expect(capabilities.mutation).not.toHaveBeenCalled();
});
it("is only applicable when a domain object is in context", function () {
expect(PropertiesAction.appliesTo(context)).toBeTruthy();
expect(PropertiesAction.appliesTo({})).toBeFalsy();

View File

@ -37,7 +37,6 @@ define(
mockGrandchildContext,
mockRootContext,
mockMutation,
mockPersistence,
mockType,
actionContext,
model,
@ -53,8 +52,6 @@ define(
}
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getId", "getCapability"]
@ -88,7 +85,6 @@ define(
mockGrandchildContext = jasmine.createSpyObj("context", ["getParent"]);
mockRootContext = jasmine.createSpyObj("context", ["getParent"]);
mockMutation = jasmine.createSpyObj("mutation", ["invoke"]);
mockPersistence = jasmine.createSpyObj("persistence", ["persist"]);
mockType = jasmine.createSpyObj("type", ["hasFeature"]);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
@ -109,7 +105,6 @@ define(
capabilities = {
mutation: mockMutation,
persistence: mockPersistence,
type: mockType
};
model = {
@ -118,7 +113,7 @@ define(
actionContext = { domainObject: mockDomainObject };
action = new RemoveAction(mockQ, mockNavigationService, actionContext);
action = new RemoveAction(mockNavigationService, actionContext);
});
it("only applies to objects with parents", function () {
@ -154,9 +149,6 @@ define(
// Should have removed "test" - that was our
// mock domain object's id.
expect(result.composition).toEqual(["a", "b"]);
// Finally, should have persisted
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("removes parent of object currently navigated to", function () {

View File

@ -30,7 +30,6 @@ define(
mockScope,
testRepresentation,
mockDomainObject,
mockPersistence,
mockStatusCapability,
mockEditorCapability,
mockCapabilities,
@ -56,15 +55,12 @@ define(
"useCapability",
"hasCapability"
]);
mockPersistence =
jasmine.createSpyObj("persistence", ["persist"]);
mockStatusCapability =
jasmine.createSpyObj("statusCapability", ["listen"]);
mockEditorCapability =
jasmine.createSpyObj("editorCapability", ["isEditContextRoot"]);
mockCapabilities = {
'persistence': mockPersistence,
'status': mockStatusCapability,
'editor': mockEditorCapability
};
@ -96,7 +92,7 @@ define(
expect(representer.listenHandle).toHaveBeenCalled();
});
it("mutates and persists upon observed changes", function () {
it("mutates upon observed changes", function () {
mockScope.model = { someKey: "some value" };
mockScope.configuration = { someConfiguration: "something" };
@ -108,9 +104,6 @@ define(
jasmine.any(Function)
);
// ... and should have persisted the mutation
expect(mockPersistence.persist).toHaveBeenCalled();
// Finally, check that the provided mutation function
// includes both model and configuration
expect(

View File

@ -0,0 +1,78 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(["../../src/services/NestedTransaction"], function (NestedTransaction) {
var TRANSACTION_METHODS = ['add', 'commit', 'cancel', 'size'];
describe("A NestedTransaction", function () {
var mockTransaction,
nestedTransaction;
beforeEach(function () {
mockTransaction =
jasmine.createSpyObj('transaction', TRANSACTION_METHODS);
nestedTransaction = new NestedTransaction(mockTransaction);
});
it("exposes a Transaction's interface", function () {
TRANSACTION_METHODS.forEach(function (method) {
expect(nestedTransaction[method])
.toEqual(jasmine.any(Function));
});
});
describe("when callbacks are added", function () {
var mockCommit,
mockCancel,
remove;
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
mockCancel = jasmine.createSpy('cancel');
remove = nestedTransaction.add(mockCommit, mockCancel);
});
it("does not interact with its parent transaction", function () {
TRANSACTION_METHODS.forEach(function (method) {
expect(mockTransaction[method])
.not.toHaveBeenCalled();
});
});
describe("and the transaction is committed", function () {
beforeEach(function () {
nestedTransaction.commit();
});
it("adds to its parent transaction", function () {
expect(mockTransaction.add).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
});
});
});
});

View File

@ -57,8 +57,7 @@ define(
transactionService.startTransaction();
transactionService.addToTransaction(onCommit, onCancel);
expect(transactionService.onCommits.length).toBe(1);
expect(transactionService.onCancels.length).toBe(1);
expect(transactionService.size()).toBe(1);
});
it("size function returns size of commit and cancel queues", function () {
@ -85,7 +84,7 @@ define(
});
it("commit calls all queued commit functions", function () {
expect(transactionService.onCommits.length).toBe(3);
expect(transactionService.size()).toBe(3);
transactionService.commit();
onCommits.forEach(function (spy) {
expect(spy).toHaveBeenCalled();
@ -95,8 +94,8 @@ define(
it("commit resets active state and clears queues", function () {
transactionService.commit();
expect(transactionService.isActive()).toBe(false);
expect(transactionService.onCommits.length).toBe(0);
expect(transactionService.onCancels.length).toBe(0);
expect(transactionService.size()).toBe(0);
expect(transactionService.size()).toBe(0);
});
});
@ -116,7 +115,7 @@ define(
});
it("cancel calls all queued cancel functions", function () {
expect(transactionService.onCancels.length).toBe(3);
expect(transactionService.size()).toBe(3);
transactionService.cancel();
onCancels.forEach(function (spy) {
expect(spy).toHaveBeenCalled();
@ -126,8 +125,7 @@ define(
it("cancel resets active state and clears queues", function () {
transactionService.cancel();
expect(transactionService.isActive()).toBe(false);
expect(transactionService.onCommits.length).toBe(0);
expect(transactionService.onCancels.length).toBe(0);
expect(transactionService.size()).toBe(0);
});
});

View File

@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/services/Transaction"],
function (Transaction) {
describe("A Transaction", function () {
var mockLog,
transaction;
beforeEach(function () {
mockLog = jasmine.createSpyObj(
'$log',
['warn', 'info', 'error', 'debug']
);
transaction = new Transaction(mockLog);
});
it("initially has a size of zero", function () {
expect(transaction.size()).toEqual(0);
});
describe("when callbacks are added", function () {
var mockCommit,
mockCancel,
remove;
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
mockCancel = jasmine.createSpy('cancel');
remove = transaction.add(mockCommit, mockCancel);
});
it("reports a new size", function () {
expect(transaction.size()).toEqual(1);
});
it("returns a function to remove those callbacks", function () {
expect(remove).toEqual(jasmine.any(Function));
remove();
expect(transaction.size()).toEqual(0);
});
describe("and the transaction is committed", function () {
beforeEach(function () {
transaction.commit();
});
it("triggers the commit callback", function () {
expect(mockCommit).toHaveBeenCalled();
});
it("does not trigger the cancel callback", function () {
expect(mockCancel).not.toHaveBeenCalled();
});
});
describe("and the transaction is cancelled", function () {
beforeEach(function () {
transaction.cancel();
});
it("triggers the cancel callback", function () {
expect(mockCancel).toHaveBeenCalled();
});
it("does not trigger the commit callback", function () {
expect(mockCommit).not.toHaveBeenCalled();
});
});
describe("and an exception is encountered during commit", function () {
beforeEach(function () {
mockCommit.andCallFake(function () {
throw new Error("test error");
});
transaction.commit();
});
it("logs an error", function () {
expect(mockLog.error).toHaveBeenCalled();
});
});
});
});
}
);

View File

@ -46,6 +46,7 @@ define([
"./src/capabilities/MutationCapability",
"./src/capabilities/DelegationCapability",
"./src/capabilities/InstantiationCapability",
"./src/runs/TransactingMutationListener",
"./src/services/Now",
"./src/services/Throttle",
"./src/services/Topic",
@ -78,6 +79,7 @@ define([
MutationCapability,
DelegationCapability,
InstantiationCapability,
TransactingMutationListener,
Now,
Throttle,
Topic,
@ -417,6 +419,12 @@ define([
}
}
],
"runs": [
{
"implementation": TransactingMutationListener,
"depends": ["topic", "transactionService"]
}
],
"constants": [
{
"key": "PERSISTENCE_SPACE",

View File

@ -49,8 +49,7 @@ define(
}
/**
* Add a domain object to the composition of the field.
* This mutates but does not persist the modified object.
* Add a domain object to the composition of this domain object.
*
* If no index is given, this is added to the end of the composition.
*

View File

@ -113,11 +113,16 @@ define(
domainObject = this.domainObject,
model = domainObject.getModel(),
modified = model.modified,
persisted = model.persisted,
persistenceService = this.persistenceService,
persistenceFn = model.persisted !== undefined ?
persistenceFn = persisted !== undefined ?
this.persistenceService.updateObject :
this.persistenceService.createObject;
if (persisted !== undefined && persisted === modified) {
return this.$q.when(true);
}
// Update persistence timestamp...
domainObject.useCapability("mutation", function (m) {
m.persisted = modified;
@ -178,6 +183,15 @@ define(
};
/**
* Check if this domain object has been persisted at some
* point.
* @returns {boolean} true if the object has been persisted
*/
PersistenceCapability.prototype.persisted = function () {
return this.domainObject.getModel().persisted !== undefined;
};
/**
* Get the key for this domain object in the given space.
*

View File

@ -0,0 +1,54 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([], function () {
/**
* Listens for mutation on domain objects and triggers persistence when
* it occurs.
* @param {Topic} topic the `topic` service; used to listen for mutation
* @memberof platform/core
*/
function TransactingMutationListener(topic, transactionService) {
var mutationTopic = topic('mutation');
mutationTopic.listen(function (domainObject) {
var persistence = domainObject.getCapability('persistence');
var wasActive = transactionService.isActive();
if (persistence.persisted()) {
if (!wasActive) {
transactionService.startTransaction();
}
transactionService.addToTransaction(
persistence.persist.bind(persistence),
persistence.refresh.bind(persistence)
);
if (!wasActive) {
transactionService.commit();
}
}
});
}
return TransactingMutationListener;
});

View File

@ -0,0 +1,120 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/runs/TransactingMutationListener"],
function (TransactingMutationListener) {
describe("TransactingMutationListener", function () {
var mockTopic,
mockMutationTopic,
mockTransactionService,
mockDomainObject,
mockPersistence;
beforeEach(function () {
mockTopic = jasmine.createSpy('topic');
mockMutationTopic =
jasmine.createSpyObj('mutation', ['listen']);
mockTransactionService =
jasmine.createSpyObj('transactionService', [
'isActive',
'startTransaction',
'addToTransaction',
'commit'
]);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId', 'getCapability', 'getModel']
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist', 'refresh', 'persisted']
);
mockTopic.andCallFake(function (t) {
return (t === 'mutation') && mockMutationTopic;
});
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockPersistence.persisted.andReturn(true);
return new TransactingMutationListener(
mockTopic,
mockTransactionService
);
});
it("listens for mutation", function () {
expect(mockMutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
[false, true].forEach(function (isActive) {
var verb = isActive ? "is" : "isn't";
function onlyWhenInactive(expectation) {
return isActive ? expectation.not : expectation;
}
describe("when a transaction " + verb + " active", function () {
var innerVerb = isActive ? "does" : "doesn't";
beforeEach(function () {
mockTransactionService.isActive.andReturn(isActive);
});
describe("and mutation occurs", function () {
beforeEach(function () {
mockMutationTopic.listen.mostRecentCall
.args[0](mockDomainObject);
});
it(innerVerb + " start a new transaction", function () {
onlyWhenInactive(
expect(mockTransactionService.startTransaction)
).toHaveBeenCalled();
});
it("adds to the active transaction", function () {
expect(mockTransactionService.addToTransaction)
.toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
it(innerVerb + " immediately commit", function () {
onlyWhenInactive(
expect(mockTransactionService.commit)
).toHaveBeenCalled();
});
});
});
});
});
}
);

View File

@ -76,17 +76,12 @@ define(
* completes.
*/
LocationCapability.prototype.setPrimaryLocation = function (location) {
var capability = this;
return this.domainObject.useCapability(
'mutation',
function (model) {
model.location = location;
}
).then(function () {
return capability.domainObject
.getCapability('persistence')
.persist();
});
);
};
/**

View File

@ -63,14 +63,7 @@ define(
);
}
return parentObject.getCapability('composition').add(object)
.then(function (objectInNewContext) {
return parentObject.getCapability('persistence')
.persist()
.then(function () {
return objectInNewContext;
});
});
return parentObject.getCapability('composition').add(object);
};
return LinkService;

View File

@ -33,7 +33,6 @@ define(
describe("instantiated with domain object", function () {
var locationCapability,
persistencePromise,
mutationPromise,
mockQ,
mockInjector,
@ -49,10 +48,6 @@ define(
return domainObjectFactory({id: 'root'});
}
},
persistence: jasmine.createSpyObj(
'persistenceCapability',
['persist']
),
mutation: jasmine.createSpyObj(
'mutationCapability',
['invoke']
@ -65,11 +60,6 @@ define(
mockObjectService =
jasmine.createSpyObj("objectService", ["getObjects"]);
persistencePromise = new ControlledPromise();
domainObject.capabilities.persistence.persist.andReturn(
persistencePromise
);
mutationPromise = new ControlledPromise();
domainObject.capabilities.mutation.invoke.andCallFake(
function (mutator) {
@ -103,22 +93,17 @@ define(
expect(locationCapability.isOriginal()).toBe(false);
});
it("can persist location", function () {
var persistResult = locationCapability
it("can mutate location", function () {
var result = locationCapability
.setPrimaryLocation('root'),
whenComplete = jasmine.createSpy('whenComplete');
persistResult.then(whenComplete);
result.then(whenComplete);
expect(domainObject.model.location).not.toBeDefined();
mutationPromise.resolve();
expect(domainObject.model.location).toBe('root');
expect(whenComplete).not.toHaveBeenCalled();
expect(domainObject.capabilities.persistence.persist)
.toHaveBeenCalled();
persistencePromise.resolve();
expect(whenComplete).toHaveBeenCalled();
});

View File

@ -139,20 +139,12 @@ define(
parentModel,
parentObject,
compositionPromise,
persistencePromise,
addPromise,
compositionCapability,
persistenceCapability;
compositionCapability;
beforeEach(function () {
compositionPromise = new ControlledPromise();
persistencePromise = new ControlledPromise();
addPromise = new ControlledPromise();
persistenceCapability = jasmine.createSpyObj(
'persistenceCapability',
['persist']
);
persistenceCapability.persist.andReturn(persistencePromise);
compositionCapability = jasmine.createSpyObj(
'compositionCapability',
['invoke', 'add']
@ -172,7 +164,6 @@ define(
return new ControlledPromise();
}
},
persistence: persistenceCapability,
composition: compositionCapability
}
});
@ -197,15 +188,6 @@ define(
.toHaveBeenCalledWith(object);
});
it("persists parent", function () {
linkService.perform(object, parentObject);
expect(addPromise.then).toHaveBeenCalled();
addPromise.resolve(linkedObject);
expect(parentObject.getCapability)
.toHaveBeenCalledWith('persistence');
expect(persistenceCapability.persist).toHaveBeenCalled();
});
it("returns object representing new link", function () {
var returnPromise, whenComplete;
returnPromise = linkService.perform(object, parentObject);
@ -213,7 +195,6 @@ define(
returnPromise.then(whenComplete);
addPromise.resolve(linkedObject);
persistencePromise.resolve();
compositionPromise.resolve([linkedObject]);
expect(whenComplete).toHaveBeenCalledWith(linkedObject);
});

View File

@ -49,17 +49,11 @@ define(
var domainObject = this.domainObject,
now = this.now;
function doPersist() {
var persistence = domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
function setTimestamp(model) {
model.timestamp = now();
}
return domainObject.useCapability('mutation', setTimestamp)
.then(doPersist);
return domainObject.useCapability('mutation', setTimestamp);
};
return AbstractStartTimerAction;

View File

@ -27,7 +27,6 @@ define(
describe("A timer's start/restart action", function () {
var mockNow,
mockDomainObject,
mockPersistence,
testModel,
action;
@ -45,14 +44,7 @@ define(
'domainObject',
['getCapability', 'useCapability']
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
@ -67,18 +59,16 @@ define(
});
});
it("updates the model with a timestamp and persists", function () {
it("updates the model with a timestamp", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(12000);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("does not truncate milliseconds", function () {
mockNow.andReturn(42321);
action.perform();
expect(testModel.timestamp).toEqual(42321);
expect(mockPersistence.persist).toHaveBeenCalled();
});
});
}

View File

@ -27,7 +27,6 @@ define(
describe("A timer's restart action", function () {
var mockNow,
mockDomainObject,
mockPersistence,
testModel,
testContext,
action;
@ -46,14 +45,7 @@ define(
'domainObject',
['getCapability', 'useCapability', 'getModel']
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
@ -70,11 +62,10 @@ define(
action = new RestartTimerAction(mockNow, testContext);
});
it("updates the model with a timestamp and persists", function () {
it("updates the model with a timestamp", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(12000);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("applies only to timers with a target time", function () {

View File

@ -27,7 +27,6 @@ define(
describe("A timer's start action", function () {
var mockNow,
mockDomainObject,
mockPersistence,
testModel,
testContext,
action;
@ -46,14 +45,7 @@ define(
'domainObject',
['getCapability', 'useCapability', 'getModel']
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
@ -70,11 +62,10 @@ define(
action = new StartTimerAction(mockNow, testContext);
});
it("updates the model with a timestamp and persists", function () {
it("updates the model with a timestamp", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(12000);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("applies only to timers without a target time", function () {

View File

@ -35,7 +35,6 @@ define(
*/
function TimelineDragHandler(domainObject, objectLoader) {
var timespans = {},
persists = {},
mutations = {},
compositions = {},
dirty = {};
@ -56,8 +55,6 @@ define(
timespans[id] = timespan;
// And its mutation capability
mutations[id] = object.getCapability('mutation');
// Also cache the persistence capability for later
persists[id] = object.getCapability('persistence');
// And the composition, for bulk moves
compositions[id] = object.getModel().composition || [];
});
@ -71,19 +68,14 @@ define(
}
// Persist changes for objects by id (when dragging ends)
function doPersist(id) {
var persistence = persists[id],
mutation = mutations[id];
function finalMutate(id) {
var mutation = mutations[id];
if (mutation) {
// Mutate just to update the timestamp (since we
// explicitly don't do this during the drag to
// avoid firing a ton of refreshes.)
mutation.mutate(function () {});
}
if (persistence) {
// Persist the changes
persistence.persist();
}
}
// Use the object loader to get objects which have timespans
@ -105,7 +97,7 @@ define(
*/
persist: function () {
// Persist every dirty object...
Object.keys(dirty).forEach(doPersist);
Object.keys(dirty).forEach(finalMutate);
// Clear out the dirty list
dirty = {};
},

View File

@ -35,7 +35,6 @@ define(
var domainObject = swimlane && swimlane.domainObject,
model = (domainObject && domainObject.getModel()) || {},
mutator = domainObject && domainObject.getCapability('mutation'),
persister = domainObject && domainObject.getCapability('persistence'),
type = domainObject && domainObject.getCapability('type'),
dropHandler = new TimelineSwimlaneDropHandler(swimlane);
@ -48,7 +47,7 @@ define(
mutator.mutate(function (m) {
m.relationships = m.relationships || {};
m.relationships[ACTIVITY_RELATIONSHIP] = value;
}).then(persister.persist);
});
}
}
// ...otherwise, use as a getter
@ -63,7 +62,7 @@ define(
// Update the link
mutator.mutate(function (m) {
m.link = value;
}).then(persister.persist);
});
}
return model.link;
}
@ -84,7 +83,7 @@ define(
}
// Activities should have the Activity Modes and Activity Link dialog
if (type && type.instanceOf("activity") && mutator && persister) {
if (type && type.instanceOf("activity") && mutator) {
swimlane.modes = modes;
swimlane.link = link;
}

View File

@ -29,16 +29,6 @@ define(
* @constructor
*/
function TimelineSwimlaneDropHandler(swimlane) {
// Utility function; like $q.when, but synchronous (to reduce
// performance impact when wrapping synchronous values)
function asPromise(value) {
return (value && value.then) ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
// Check if we are in edit mode (also check parents)
function inEditMode() {
return swimlane.domainObject.hasCapability('editor') &&
@ -75,16 +65,7 @@ define(
// Initiate mutation of a domain object
function doMutate(domainObject, mutator) {
return asPromise(
domainObject.useCapability("mutation", mutator)
).then(function () {
// Persist the results of mutation
var persistence = domainObject.getCapability("persistence");
if (persistence) {
// Persist the changes
persistence.persist();
}
});
return domainObject.useCapability("mutation", mutator);
}
// Check if this swimlane is in a state where a drop-after will

View File

@ -32,7 +32,6 @@ define(
mockDomainObjects,
mockTimespans,
mockMutations,
mockPersists,
mockCallback,
handler;
@ -66,7 +65,6 @@ define(
mockDomainObj.useCapability.andReturn(asPromise(mockTimespans[id]));
mockDomainObj.getCapability.andCallFake(function (c) {
return {
persistence: mockPersists[id],
mutation: mockMutations[id]
}[c];
});
@ -76,17 +74,12 @@ define(
beforeEach(function () {
mockTimespans = {};
mockPersists = {};
mockMutations = {};
['a', 'b', 'c', 'd', 'e', 'f'].forEach(function (id, index) {
mockTimespans[id] = jasmine.createSpyObj(
'timespan-' + id,
['getStart', 'getEnd', 'getDuration', 'setStart', 'setEnd', 'setDuration']
);
mockPersists[id] = jasmine.createSpyObj(
'persistence-' + id,
['persist']
);
mockMutations[id] = jasmine.createSpyObj(
'mutation-' + id,
['mutate']
@ -209,20 +202,6 @@ define(
expect(mockTimespans.c.setStart).toHaveBeenCalledWith(1000);
});
it("persists mutated objects", function () {
handler.start('a', 20);
handler.end('b', 50);
handler.duration('c', 30);
handler.persist();
expect(mockPersists.a.persist).toHaveBeenCalled();
expect(mockPersists.b.persist).toHaveBeenCalled();
expect(mockPersists.c.persist).toHaveBeenCalled();
expect(mockPersists.d.persist).not.toHaveBeenCalled();
expect(mockPersists.e.persist).not.toHaveBeenCalled();
expect(mockPersists.f.persist).not.toHaveBeenCalled();
});
});
}
);

View File

@ -50,10 +50,6 @@ define(
'mutation',
['mutate']
);
mockCapabilities.persistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockCapabilities.type = jasmine.createSpyObj(
'type',
['instanceOf']
@ -115,11 +111,6 @@ define(
.toHaveBeenCalledWith(jasmine.any(Function));
mockCapabilities.mutation.mutate.mostRecentCall.args[0](testModel);
expect(testModel.relationships.modes).toEqual(['abc', 'xyz']);
// Verify that persistence is called when promise resolves
expect(mockCapabilities.persistence.persist).not.toHaveBeenCalled();
mockPromise.then.mostRecentCall.args[0]();
expect(mockCapabilities.persistence.persist).toHaveBeenCalled();
});
it("mutates modes when used as a setter", function () {
@ -128,11 +119,6 @@ define(
.toHaveBeenCalledWith(jasmine.any(Function));
mockCapabilities.mutation.mutate.mostRecentCall.args[0](testModel);
expect(testModel.link).toEqual("http://www.noaa.gov");
// Verify that persistence is called when promise resolves
expect(mockCapabilities.persistence.persist).not.toHaveBeenCalled();
mockPromise.then.mostRecentCall.args[0]();
expect(mockCapabilities.persistence.persist).toHaveBeenCalled();
});
it("does not mutate modes when unchanged", function () {

View File

@ -29,7 +29,6 @@ define(
mockOtherObject,
mockActionCapability,
mockEditorCapability,
mockPersistence,
mockContext,
mockAction,
handler;
@ -76,7 +75,6 @@ define(
["getId", "getCapability", "useCapability", "hasCapability"]
);
mockActionCapability = jasmine.createSpyObj("action", ["perform", "getActions"]);
mockPersistence = jasmine.createSpyObj("persistence", ["persist"]);
mockContext = jasmine.createSpyObj('context', ['getParent']);
mockActionCapability.getActions.andReturn([mockAction]);
@ -89,14 +87,12 @@ define(
mockSwimlane.domainObject.getCapability.andCallFake(function (c) {
return {
action: mockActionCapability,
persistence: mockPersistence,
editor: mockEditorCapability
}[c];
});
mockSwimlane.parent.domainObject.getCapability.andCallFake(function (c) {
return {
action: mockActionCapability,
persistence: mockPersistence,
editor: mockEditorCapability
}[c];
});
@ -162,8 +158,6 @@ define(
mockSwimlane.domainObject.useCapability.mostRecentCall
.args[1](testModel);
expect(testModel.composition).toEqual(['c', 'd']);
// Finally, should also have persisted
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("inserts after as a peer when highlighted at the bottom", function () {