mirror of
https://github.com/nasa/openmct.git
synced 2025-01-20 11:38:56 +00:00
Merge branch 'open1033' into open-master
This commit is contained in:
commit
58d66871c1
@ -12,8 +12,8 @@
|
||||
"platform/features/plot",
|
||||
"platform/features/scrolling",
|
||||
"platform/forms",
|
||||
"platform/persistence/cache",
|
||||
"platform/persistence/couch",
|
||||
"platform/persistence/queue",
|
||||
"platform/persistence/elastic",
|
||||
|
||||
"example/generator"
|
||||
]
|
||||
|
27
platform/commonUI/dialog/README.md
Normal file
27
platform/commonUI/dialog/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
This bundle provides `dialogService`, which can be used to prompt
|
||||
for user input.
|
||||
|
||||
## `getUserChoice`
|
||||
|
||||
The `getUserChoice` method is useful for displaying a message and a set of
|
||||
buttons. This method returns a promise which will resolve to the user's
|
||||
chosen option (or, more specifically, its `key`), and will be rejected if
|
||||
the user closes the dialog with the X in the top-right;
|
||||
|
||||
The `dialogModel` given as an argument to this method should have the
|
||||
following properties.
|
||||
|
||||
* `title`: The title to display at the top of the dialog.
|
||||
* `hint`: Short message to display below the title.
|
||||
* `template`: Identifying key (as will be passed to `mct-include`) for
|
||||
the template which will be used to populate the inner area of the dialog.
|
||||
* `model`: Model to pass in the `ng-model` attribute of
|
||||
`mct-include`.
|
||||
* `parameters`: Parameters to pass in the `parameters` attribute of
|
||||
`mct-include`.
|
||||
* `options`: An array of options describing each button at the bottom.
|
||||
Each option may have the following properties:
|
||||
* `name`: Human-readable name to display in the button.
|
||||
* `key`: Machine-readable key, to pass as the result of the resolved
|
||||
promise when clicked.
|
||||
* `description`: Description to show in tool tip on hover.
|
@ -17,6 +17,10 @@
|
||||
"key": "overlay-dialog",
|
||||
"templateUrl": "templates/overlay-dialog.html"
|
||||
},
|
||||
{
|
||||
"key": "overlay-options",
|
||||
"templateUrl": "templates/overlay-options.html"
|
||||
},
|
||||
{
|
||||
"key": "form-dialog",
|
||||
"templateUrl": "templates/dialog.html"
|
||||
|
24
platform/commonUI/dialog/res/templates/overlay-options.html
Normal file
24
platform/commonUI/dialog/res/templates/overlay-options.html
Normal file
@ -0,0 +1,24 @@
|
||||
<mct-container key="overlay">
|
||||
<div class="abs top-bar">
|
||||
<div class="title">{{ngModel.dialog.title}}</div>
|
||||
<div class="hint">{{ngModel.dialog.hint}}</div>
|
||||
</div>
|
||||
<div class="abs form outline editor">
|
||||
<div class='abs contents l-dialog'>
|
||||
<mct-include key="ngModel.dialog.template"
|
||||
parameters="ngModel.dialog.parameters"
|
||||
ng-model="ngModel.dialog.model">
|
||||
</mct-include>
|
||||
</div>
|
||||
</div>
|
||||
<div class="abs bottom-bar">
|
||||
<a ng-repeat="option in ngModel.dialog.options"
|
||||
href=''
|
||||
class="btn lg"
|
||||
title="{{option.description}}"
|
||||
ng-click="ngModel.confirm(option.key)"
|
||||
ng-class="{ major: $first, subtle: !$first }">
|
||||
{{option.name}}
|
||||
</a>
|
||||
</div>
|
||||
</mct-container>
|
@ -26,7 +26,7 @@ define(
|
||||
dialogVisible = false;
|
||||
}
|
||||
|
||||
function getUserInput(formModel, value) {
|
||||
function getDialogResponse(key, model, resultGetter) {
|
||||
// We will return this result as a promise, because user
|
||||
// input is asynchronous.
|
||||
var deferred = $q.defer(),
|
||||
@ -35,9 +35,9 @@ define(
|
||||
// Confirm function; this will be passed in to the
|
||||
// overlay-dialog template and associated with a
|
||||
// OK button click
|
||||
function confirm() {
|
||||
function confirm(value) {
|
||||
// Pass along the result
|
||||
deferred.resolve(overlayModel.value);
|
||||
deferred.resolve(resultGetter ? resultGetter() : value);
|
||||
|
||||
// Stop showing the dialog
|
||||
dismiss();
|
||||
@ -51,6 +51,10 @@ define(
|
||||
dismiss();
|
||||
}
|
||||
|
||||
// Add confirm/cancel callbacks
|
||||
model.confirm = confirm;
|
||||
model.cancel = cancel;
|
||||
|
||||
if (dialogVisible) {
|
||||
// Only one dialog should be shown at a time.
|
||||
// The application design should be such that
|
||||
@ -58,26 +62,15 @@ define(
|
||||
$log.warn([
|
||||
"Dialog already showing; ",
|
||||
"unable to show ",
|
||||
formModel.name
|
||||
model.name
|
||||
].join(""));
|
||||
deferred.reject();
|
||||
} else {
|
||||
// To be passed to the overlay-dialog template,
|
||||
// via ng-model
|
||||
overlayModel = {
|
||||
title: formModel.name,
|
||||
message: formModel.message,
|
||||
structure: formModel,
|
||||
value: value,
|
||||
confirm: confirm,
|
||||
cancel: cancel
|
||||
};
|
||||
|
||||
// Add the overlay using the OverlayService, which
|
||||
// will handle actual insertion into the DOM
|
||||
overlay = overlayService.createOverlay(
|
||||
"overlay-dialog",
|
||||
overlayModel
|
||||
key,
|
||||
model
|
||||
);
|
||||
|
||||
// Track that a dialog is already visible, to
|
||||
@ -88,6 +81,35 @@ define(
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function getUserInput(formModel, value) {
|
||||
var overlayModel = {
|
||||
title: formModel.name,
|
||||
message: formModel.message,
|
||||
structure: formModel,
|
||||
value: value
|
||||
};
|
||||
|
||||
// Provide result from the model
|
||||
function resultGetter() {
|
||||
return overlayModel.value;
|
||||
}
|
||||
|
||||
// Show the overlay-dialog
|
||||
return getDialogResponse(
|
||||
"overlay-dialog",
|
||||
overlayModel,
|
||||
resultGetter
|
||||
);
|
||||
}
|
||||
|
||||
function getUserChoice(dialogModel) {
|
||||
// Show the overlay-options dialog
|
||||
return getDialogResponse(
|
||||
"overlay-options",
|
||||
{ dialog: dialogModel }
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Request user input via a window-modal dialog.
|
||||
@ -100,7 +122,14 @@ define(
|
||||
* user input cannot be obtained (for instance,
|
||||
* because the user cancelled the dialog)
|
||||
*/
|
||||
getUserInput: getUserInput
|
||||
getUserInput: getUserInput,
|
||||
/**
|
||||
* Request that the user chooses from a set of options,
|
||||
* which will be shown as buttons.
|
||||
*
|
||||
* @param dialogModel a description of the dialog to show
|
||||
*/
|
||||
getUserChoice: getUserChoice
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -86,6 +86,19 @@ define(
|
||||
expect(mockDeferred.reject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("provides an options dialogs", function () {
|
||||
var dialogModel = {};
|
||||
dialogService.getUserChoice(dialogModel);
|
||||
expect(mockOverlayService.createOverlay).toHaveBeenCalledWith(
|
||||
'overlay-options',
|
||||
{
|
||||
dialog: dialogModel,
|
||||
confirm: jasmine.any(Function),
|
||||
cancel: jasmine.any(Function)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -10,7 +10,7 @@
|
||||
{
|
||||
"key": "EditController",
|
||||
"implementation": "controllers/EditController.js",
|
||||
"depends": [ "$scope", "navigationService" ]
|
||||
"depends": [ "$scope", "$q", "navigationService" ]
|
||||
},
|
||||
{
|
||||
"key": "EditActionController",
|
||||
|
@ -13,24 +13,18 @@ define(
|
||||
function SaveAction($location, context) {
|
||||
var domainObject = context.domainObject;
|
||||
|
||||
// Look up the object's "editor.completion" capability;
|
||||
// Invoke any save behavior introduced by the editor capability;
|
||||
// this is introduced by EditableDomainObject which is
|
||||
// used to insulate underlying objects from changes made
|
||||
// during editing.
|
||||
function getEditorCapability() {
|
||||
return domainObject.getCapability("editor");
|
||||
}
|
||||
|
||||
// Invoke any save behavior introduced by the editor.completion
|
||||
// capability.
|
||||
function doSave(editor) {
|
||||
return editor.save();
|
||||
function doSave() {
|
||||
return domainObject.getCapability("editor").save();
|
||||
}
|
||||
|
||||
// Discard the current root view (which will be the editing
|
||||
// UI, which will have been pushed atop the Browise UI.)
|
||||
function returnToBrowse() {
|
||||
$location.path("/browse");
|
||||
return $location.path("/browse");
|
||||
}
|
||||
|
||||
return {
|
||||
@ -41,7 +35,7 @@ define(
|
||||
* cancellation has completed
|
||||
*/
|
||||
perform: function () {
|
||||
return doSave(getEditorCapability()).then(returnToBrowse);
|
||||
return doSave().then(returnToBrowse);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -29,6 +29,13 @@ define(
|
||||
cache.markDirty(editableObject);
|
||||
};
|
||||
|
||||
// Delegate refresh to the original object; this avoids refreshing
|
||||
// the editable instance of the object, and ensures that refresh
|
||||
// correctly targets the "real" version of the object.
|
||||
persistence.refresh = function () {
|
||||
return domainObject.getCapability('persistence').refresh();
|
||||
};
|
||||
|
||||
return persistence;
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ define(
|
||||
// removed from the layer which gets dependency
|
||||
// injection.
|
||||
function resolvePromise(value) {
|
||||
return value && value.then ? value : {
|
||||
return (value && value.then) ? value : {
|
||||
then: function (callback) {
|
||||
return resolvePromise(callback(value));
|
||||
}
|
||||
@ -50,19 +50,7 @@ define(
|
||||
|
||||
// Persist the underlying domain object
|
||||
function doPersist() {
|
||||
return persistenceCapability.persist();
|
||||
}
|
||||
|
||||
// Save any other objects that have been modified in the cache.
|
||||
// IMPORTANT: This must not be called until after this object
|
||||
// has been marked as clean.
|
||||
function saveOthers() {
|
||||
return cache.saveAll();
|
||||
}
|
||||
|
||||
// Indicate that this object has been saved.
|
||||
function markClean() {
|
||||
return cache.markClean(editableObject);
|
||||
return domainObject.getCapability('persistence').persist();
|
||||
}
|
||||
|
||||
return {
|
||||
@ -70,14 +58,15 @@ define(
|
||||
* Save any changes that have been made to this domain object
|
||||
* (as well as to others that might have been retrieved and
|
||||
* modified during the editing session)
|
||||
* @param {boolean} nonrecursive if true, save only this
|
||||
* object (and not other objects with associated changes)
|
||||
* @returns {Promise} a promise that will be fulfilled after
|
||||
* persistence has completed.
|
||||
*/
|
||||
save: function () {
|
||||
return resolvePromise(doMutate())
|
||||
.then(doPersist)
|
||||
.then(markClean)
|
||||
.then(saveOthers);
|
||||
save: function (nonrecursive) {
|
||||
return nonrecursive ?
|
||||
resolvePromise(doMutate()).then(doPersist) :
|
||||
resolvePromise(cache.saveAll());
|
||||
},
|
||||
/**
|
||||
* Cancel editing; Discard any changes that have been made to
|
||||
|
@ -14,12 +14,12 @@ define(
|
||||
* navigated domain object into the scope.
|
||||
* @constructor
|
||||
*/
|
||||
function EditController($scope, navigationService) {
|
||||
function EditController($scope, $q, navigationService) {
|
||||
function setNavigation(domainObject) {
|
||||
// Wrap the domain object such that all mutation is
|
||||
// confined to edit mode (until Save)
|
||||
$scope.navigatedObject =
|
||||
domainObject && new EditableDomainObject(domainObject);
|
||||
domainObject && new EditableDomainObject(domainObject, $q);
|
||||
}
|
||||
|
||||
setNavigation(navigationService.getNavigation());
|
||||
|
@ -48,7 +48,7 @@ define(
|
||||
* and provides a "working copy" of the object's
|
||||
* model to allow changes to be easily cancelled.
|
||||
*/
|
||||
function EditableDomainObject(domainObject) {
|
||||
function EditableDomainObject(domainObject, $q) {
|
||||
// The cache will hold all domain objects reached from
|
||||
// the initial EditableDomainObject; this ensures that
|
||||
// different versions of the same editable domain object
|
||||
@ -81,7 +81,7 @@ define(
|
||||
return editableObject;
|
||||
}
|
||||
|
||||
cache = new EditableDomainObjectCache(EditableDomainObjectImpl);
|
||||
cache = new EditableDomainObjectCache(EditableDomainObjectImpl, $q);
|
||||
|
||||
return cache.getEditableObject(domainObject);
|
||||
}
|
||||
|
@ -29,10 +29,11 @@ define(
|
||||
* constructor function which takes a regular domain object as
|
||||
* an argument, and returns an editable domain object as its
|
||||
* result.
|
||||
* @param $q Angular's $q, for promise handling
|
||||
* @constructor
|
||||
* @memberof module:editor/object/editable-domain-object-cache
|
||||
*/
|
||||
function EditableDomainObjectCache(EditableDomainObject) {
|
||||
function EditableDomainObjectCache(EditableDomainObject, $q) {
|
||||
var cache = new EditableModelCache(),
|
||||
dirty = {},
|
||||
root;
|
||||
@ -88,23 +89,20 @@ define(
|
||||
* Initiate a save on all objects that have been cached.
|
||||
*/
|
||||
saveAll: function () {
|
||||
var object;
|
||||
// Get a list of all dirty objects
|
||||
var objects = Object.keys(dirty).map(function (k) {
|
||||
return dirty[k];
|
||||
});
|
||||
|
||||
// Clear dirty set, since we're about to save.
|
||||
dirty = {};
|
||||
|
||||
// Most save logic is handled by the "editor.completion"
|
||||
// capability, but this in turn will typically invoke
|
||||
// Save All. An infinite loop is avoided by marking
|
||||
// objects as clean as we go.
|
||||
|
||||
while (Object.keys(dirty).length > 0) {
|
||||
// Pick the first dirty object
|
||||
object = dirty[Object.keys(dirty)[0]];
|
||||
|
||||
// Mark non-dirty to avoid successive invocations
|
||||
this.markClean(object);
|
||||
|
||||
// Invoke its save behavior
|
||||
object.getCapability('editor').save();
|
||||
}
|
||||
// capability, so that is delegated here.
|
||||
return $q.all(objects.map(function (object) {
|
||||
// Save; pass a nonrecursive flag to avoid looping
|
||||
return object.getCapability('editor').save(true);
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ define(
|
||||
beforeEach(function () {
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
"persistence",
|
||||
[ "persist" ]
|
||||
[ "persist", "refresh" ]
|
||||
);
|
||||
mockEditableObject = jasmine.createSpyObj(
|
||||
"editableObject",
|
||||
@ -30,6 +30,8 @@ define(
|
||||
[ "markDirty" ]
|
||||
);
|
||||
|
||||
mockDomainObject.getCapability.andReturn(mockPersistence);
|
||||
|
||||
capability = new EditablePersistenceCapability(
|
||||
mockPersistence,
|
||||
mockEditableObject,
|
||||
@ -49,6 +51,18 @@ define(
|
||||
expect(mockPersistence.persist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refreshes using the original domain object's persistence", function () {
|
||||
// Refreshing needs to delegate via the unwrapped domain object.
|
||||
// Otherwise, only the editable version of the object will be updated;
|
||||
// we instead want the real version of the object to receive these
|
||||
// changes.
|
||||
expect(mockDomainObject.getCapability).not.toHaveBeenCalled();
|
||||
expect(mockPersistence.refresh).not.toHaveBeenCalled();
|
||||
capability.refresh();
|
||||
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('persistence');
|
||||
expect(mockPersistence.refresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -5,7 +5,7 @@ define(
|
||||
function (EditorCapability) {
|
||||
"use strict";
|
||||
|
||||
describe("An editable context capability", function () {
|
||||
describe("The editor capability", function () {
|
||||
var mockPersistence,
|
||||
mockEditableObject,
|
||||
mockDomainObject,
|
||||
@ -32,6 +32,8 @@ define(
|
||||
);
|
||||
mockCallback = jasmine.createSpy("callback");
|
||||
|
||||
mockDomainObject.getCapability.andReturn(mockPersistence);
|
||||
|
||||
model = { someKey: "some value", x: 42 };
|
||||
|
||||
capability = new EditorCapability(
|
||||
@ -42,8 +44,8 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
it("mutates the real domain object on save", function () {
|
||||
capability.save().then(mockCallback);
|
||||
it("mutates the real domain object on nonrecursive save", function () {
|
||||
capability.save(true).then(mockCallback);
|
||||
|
||||
// Wait for promise to resolve
|
||||
waitsFor(function () {
|
||||
@ -60,19 +62,6 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
it("marks the saved object as clean in the editing cache", function () {
|
||||
capability.save().then(mockCallback);
|
||||
|
||||
// Wait for promise to resolve
|
||||
waitsFor(function () {
|
||||
return mockCallback.calls.length > 0;
|
||||
}, 250);
|
||||
|
||||
runs(function () {
|
||||
expect(mockCache.markClean).toHaveBeenCalledWith(mockEditableObject);
|
||||
});
|
||||
});
|
||||
|
||||
it("tells the cache to save others", function () {
|
||||
capability.save().then(mockCallback);
|
||||
|
||||
|
@ -7,6 +7,7 @@ define(
|
||||
|
||||
describe("The Edit mode controller", function () {
|
||||
var mockScope,
|
||||
mockQ,
|
||||
mockNavigationService,
|
||||
mockObject,
|
||||
mockCapability,
|
||||
@ -17,6 +18,7 @@ define(
|
||||
"$scope",
|
||||
[ "$on" ]
|
||||
);
|
||||
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
|
||||
mockNavigationService = jasmine.createSpyObj(
|
||||
"navigationService",
|
||||
[ "getNavigation", "addListener", "removeListener" ]
|
||||
@ -37,6 +39,7 @@ define(
|
||||
|
||||
controller = new EditController(
|
||||
mockScope,
|
||||
mockQ,
|
||||
mockNavigationService
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
/*global define,describe,it,expect,beforeEach*/
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/objects/EditableDomainObjectCache"],
|
||||
@ -10,6 +10,7 @@ define(
|
||||
var captured,
|
||||
completionCapability,
|
||||
object,
|
||||
mockQ,
|
||||
cache;
|
||||
|
||||
|
||||
@ -33,6 +34,7 @@ define(
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
|
||||
captured = {};
|
||||
completionCapability = {
|
||||
save: function () {
|
||||
@ -40,7 +42,7 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
cache = new EditableDomainObjectCache(WrapObject);
|
||||
cache = new EditableDomainObjectCache(WrapObject, mockQ);
|
||||
});
|
||||
|
||||
it("wraps objects using provided constructor", function () {
|
||||
|
@ -65,6 +65,11 @@
|
||||
"implementation": "models/PersistedModelProvider.js",
|
||||
"depends": [ "persistenceService", "$q", "PERSISTENCE_SPACE" ]
|
||||
},
|
||||
{
|
||||
"provides": "modelService",
|
||||
"type": "decorator",
|
||||
"implementation": "models/CachingModelDecorator.js"
|
||||
},
|
||||
{
|
||||
"provides": "typeService",
|
||||
"type": "provider",
|
||||
@ -162,7 +167,8 @@
|
||||
},
|
||||
{
|
||||
"key": "mutation",
|
||||
"implementation": "capabilities/MutationCapability.js"
|
||||
"implementation": "capabilities/MutationCapability.js",
|
||||
"depends": [ "now" ]
|
||||
},
|
||||
{
|
||||
"key": "delegation",
|
||||
|
@ -50,9 +50,9 @@ define(
|
||||
* which will expose this capability
|
||||
* @constructor
|
||||
*/
|
||||
function MutationCapability(domainObject) {
|
||||
function MutationCapability(now, domainObject) {
|
||||
|
||||
function mutate(mutator) {
|
||||
function mutate(mutator, timestamp) {
|
||||
// Get the object's model and clone it, so the
|
||||
// mutator function has a temporary copy to work with.
|
||||
var model = domainObject.getModel(),
|
||||
@ -73,7 +73,8 @@ define(
|
||||
if (model !== result) {
|
||||
copyValues(model, result);
|
||||
}
|
||||
model.modified = Date.now();
|
||||
model.modified = (typeof timestamp === 'number') ?
|
||||
timestamp : now();
|
||||
}
|
||||
|
||||
// Report the result of the mutation
|
||||
@ -109,8 +110,11 @@ define(
|
||||
* handled as one of the above.
|
||||
*
|
||||
*
|
||||
* @params {function} mutator the function which will make
|
||||
* @param {function} mutator the function which will make
|
||||
* changes to the domain object's model.
|
||||
* @param {number} [timestamp] timestamp to record for
|
||||
* this mutation (otherwise, system time will be
|
||||
* used)
|
||||
* @returns {Promise.<boolean>} a promise for the result
|
||||
* of the mutation; true if changes were made.
|
||||
*/
|
||||
|
@ -22,6 +22,36 @@ define(
|
||||
* @constructor
|
||||
*/
|
||||
function PersistenceCapability(persistenceService, SPACE, domainObject) {
|
||||
// Cache modified timestamp
|
||||
var modified = domainObject.getModel().modified;
|
||||
|
||||
// Update a domain object's model upon refresh
|
||||
function updateModel(model) {
|
||||
var modified = model.modified;
|
||||
return domainObject.useCapability("mutation", function () {
|
||||
return model;
|
||||
}, modified);
|
||||
}
|
||||
|
||||
// For refresh; update a domain object model, only if there
|
||||
// are no unsaved changes.
|
||||
function updatePersistenceTimestamp() {
|
||||
var modified = domainObject.getModel().modified;
|
||||
domainObject.useCapability("mutation", function (model) {
|
||||
model.persisted = modified;
|
||||
}, modified);
|
||||
}
|
||||
|
||||
// Utility function for creating promise-like objects which
|
||||
// resolve synchronously when possible
|
||||
function fastPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return fastPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Persist any changes which have been made to this
|
||||
@ -31,12 +61,29 @@ define(
|
||||
* if not.
|
||||
*/
|
||||
persist: function () {
|
||||
updatePersistenceTimestamp();
|
||||
return persistenceService.updateObject(
|
||||
SPACE,
|
||||
domainObject.getId(),
|
||||
domainObject.getModel()
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Update this domain object to match the latest from
|
||||
* persistence.
|
||||
* @returns {Promise} a promise which will be resolved
|
||||
* when the update is complete
|
||||
*/
|
||||
refresh: function () {
|
||||
var model = domainObject.getModel();
|
||||
// Only update if we don't have unsaved changes
|
||||
return (model.modified === model.persisted) ?
|
||||
persistenceService.readObject(
|
||||
SPACE,
|
||||
domainObject.getId()
|
||||
).then(updateModel) :
|
||||
fastPromise(false);
|
||||
},
|
||||
/**
|
||||
* Get the space in which this domain object is persisted;
|
||||
* this is useful when, for example, decided which space a
|
||||
|
115
platform/core/src/models/CachingModelDecorator.js
Normal file
115
platform/core/src/models/CachingModelDecorator.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The caching model decorator maintains a cache of loaded domain
|
||||
* object models, and ensures that duplicate models for the same
|
||||
* object are not provided.
|
||||
* @constructor
|
||||
*/
|
||||
function CachingModelDecorator(modelService) {
|
||||
var cache = {},
|
||||
cached = {};
|
||||
|
||||
// Update the cached instance of a model to a new value.
|
||||
// We update in-place to ensure there is only ever one instance
|
||||
// of any given model exposed by the modelService as a whole.
|
||||
function updateModel(id, model) {
|
||||
var oldModel = cache[id];
|
||||
|
||||
// Same object instance is a possibility, so don't copy
|
||||
if (oldModel === model) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// If we'd previously cached an undefined value, or are now
|
||||
// seeing undefined, replace the item in the cache entirely.
|
||||
if (oldModel === undefined || model === undefined) {
|
||||
cache[id] = model;
|
||||
return model;
|
||||
}
|
||||
|
||||
// Otherwise, empty out the old model...
|
||||
Object.keys(oldModel).forEach(function (k) {
|
||||
delete oldModel[k];
|
||||
});
|
||||
|
||||
// ...and replace it with the contents of the new model.
|
||||
Object.keys(model).forEach(function (k) {
|
||||
oldModel[k] = model[k];
|
||||
});
|
||||
|
||||
return oldModel;
|
||||
}
|
||||
|
||||
// Fast-resolving promise
|
||||
function fastPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return fastPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Store this model in the cache
|
||||
function cacheModel(id, model) {
|
||||
cache[id] = cached[id] ? updateModel(id, model) : model;
|
||||
cached[id] = true;
|
||||
}
|
||||
|
||||
// Check if an id is not in cache, for lookup filtering
|
||||
function notCached(id) {
|
||||
return !cached[id];
|
||||
}
|
||||
|
||||
// Store the provided models in our cache
|
||||
function cacheAll(models) {
|
||||
Object.keys(models).forEach(function (id) {
|
||||
cacheModel(id, models[id]);
|
||||
});
|
||||
}
|
||||
|
||||
// Expose the cache (for promise chaining)
|
||||
function giveCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get models for these specified string identifiers.
|
||||
* These will be given as an object containing keys
|
||||
* and values, where keys are object identifiers and
|
||||
* values are models.
|
||||
* This result may contain either a subset or a
|
||||
* superset of the total objects.
|
||||
*
|
||||
* @param {Array<string>} ids the string identifiers for
|
||||
* models of interest.
|
||||
* @returns {Promise<object>} a promise for an object
|
||||
* containing key-value pairs, where keys are
|
||||
* ids and values are models
|
||||
* @method
|
||||
*/
|
||||
getModels: function (ids) {
|
||||
var neededIds = ids.filter(notCached);
|
||||
|
||||
// Look up if we have unknown IDs
|
||||
if (neededIds.length > 0) {
|
||||
return modelService.getModels(neededIds)
|
||||
.then(cacheAll)
|
||||
.then(giveCache);
|
||||
}
|
||||
|
||||
// Otherwise, just expose the cache directly
|
||||
return fastPromise(cache);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return CachingModelDecorator;
|
||||
}
|
||||
);
|
@ -18,6 +18,14 @@ define(
|
||||
*/
|
||||
function ModelAggregator($q, providers) {
|
||||
|
||||
// Pick a domain object model to use, favoring the one
|
||||
// with the most recent timestamp
|
||||
function pick(a, b) {
|
||||
var aModified = (a || {}).modified || Number.NEGATIVE_INFINITY,
|
||||
bModified = (b || {}).modified || Number.NEGATIVE_INFINITY;
|
||||
return (aModified > bModified) ? a : (b || a);
|
||||
}
|
||||
|
||||
// Merge results from multiple providers into one
|
||||
// large result object.
|
||||
function mergeModels(provided, ids) {
|
||||
@ -25,7 +33,7 @@ define(
|
||||
ids.forEach(function (id) {
|
||||
provided.forEach(function (models) {
|
||||
if (models[id]) {
|
||||
result[id] = models[id];
|
||||
result[id] = pick(result[id], models[id]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -10,12 +10,15 @@ define(
|
||||
|
||||
describe("The mutation capability", function () {
|
||||
var testModel,
|
||||
mockNow,
|
||||
domainObject = { getModel: function () { return testModel; } },
|
||||
mutation;
|
||||
|
||||
beforeEach(function () {
|
||||
testModel = { number: 6 };
|
||||
mutation = new MutationCapability(domainObject);
|
||||
mockNow = jasmine.createSpy('now');
|
||||
mockNow.andReturn(12321);
|
||||
mutation = new MutationCapability(mockNow, domainObject);
|
||||
});
|
||||
|
||||
it("allows mutation of a model", function () {
|
||||
@ -41,6 +44,24 @@ define(
|
||||
// Number should not have been changed
|
||||
expect(testModel.number).toEqual(6);
|
||||
});
|
||||
|
||||
it("attaches a timestamp on mutation", function () {
|
||||
// Verify precondition
|
||||
expect(testModel.modified).toBeUndefined();
|
||||
mutation.invoke(function (m) {
|
||||
m.number = m.number * 7;
|
||||
});
|
||||
// Should have gotten a timestamp from 'now'
|
||||
expect(testModel.modified).toEqual(12321);
|
||||
});
|
||||
|
||||
it("allows a timestamp to be provided", function () {
|
||||
mutation.invoke(function (m) {
|
||||
m.number = m.number * 7;
|
||||
}, 42);
|
||||
// Should have gotten a timestamp from 'now'
|
||||
expect(testModel.modified).toEqual(42);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -16,15 +16,30 @@ define(
|
||||
SPACE = "some space",
|
||||
persistence;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockPersistenceService = jasmine.createSpyObj(
|
||||
"persistenceService",
|
||||
[ "updateObject" ]
|
||||
[ "updateObject", "readObject" ]
|
||||
);
|
||||
mockDomainObject = {
|
||||
getId: function () { return id; },
|
||||
getModel: function () { return model; }
|
||||
getModel: function () { return model; },
|
||||
useCapability: jasmine.createSpy()
|
||||
};
|
||||
// Simulate mutation capability
|
||||
mockDomainObject.useCapability.andCallFake(function (capability, mutator) {
|
||||
if (capability === 'mutation') {
|
||||
model = mutator(model) || model;
|
||||
}
|
||||
});
|
||||
persistence = new PersistenceCapability(
|
||||
mockPersistenceService,
|
||||
SPACE,
|
||||
@ -49,6 +64,31 @@ define(
|
||||
expect(persistence.getSpace()).toEqual(SPACE);
|
||||
});
|
||||
|
||||
it("updates persisted timestamp on persistence", function () {
|
||||
model.modified = 12321;
|
||||
persistence.persist();
|
||||
expect(model.persisted).toEqual(12321);
|
||||
});
|
||||
|
||||
it("refreshes the domain object model from persistence", function () {
|
||||
var refreshModel = { someOtherKey: "some other value" };
|
||||
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
|
||||
persistence.refresh();
|
||||
expect(model).toEqual(refreshModel);
|
||||
});
|
||||
|
||||
it("does not overwrite unpersisted changes on refresh", function () {
|
||||
var refreshModel = { someOtherKey: "some other value" },
|
||||
mockCallback = jasmine.createSpy();
|
||||
model.modified = 2;
|
||||
model.persisted = 1;
|
||||
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
|
||||
persistence.refresh().then(mockCallback);
|
||||
expect(model).not.toEqual(refreshModel);
|
||||
// Should have also indicated that no changes were actually made
|
||||
expect(mockCallback).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
132
platform/core/test/models/CachingModelDecoratorSpec.js
Normal file
132
platform/core/test/models/CachingModelDecoratorSpec.js
Normal file
@ -0,0 +1,132 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/models/CachingModelDecorator"],
|
||||
function (CachingModelDecorator) {
|
||||
"use strict";
|
||||
|
||||
describe("The caching model decorator", function () {
|
||||
var mockModelService,
|
||||
mockCallback,
|
||||
testModels,
|
||||
decorator;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fakePromise() {
|
||||
var chains = [],
|
||||
callbacks = [];
|
||||
|
||||
return {
|
||||
then: function (callback) {
|
||||
var next = fakePromise();
|
||||
callbacks.push(callback);
|
||||
chains.push(next);
|
||||
return next;
|
||||
},
|
||||
resolve: function (value) {
|
||||
callbacks.forEach(function (cb, i) {
|
||||
chains[i].resolve(cb(value));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockCallback = jasmine.createSpy();
|
||||
mockModelService = jasmine.createSpyObj('modelService', ['getModels']);
|
||||
testModels = {
|
||||
a: { someKey: "some value" },
|
||||
b: { someOtherKey: "some other value" }
|
||||
};
|
||||
mockModelService.getModels.andReturn(asPromise(testModels));
|
||||
decorator = new CachingModelDecorator(mockModelService);
|
||||
});
|
||||
|
||||
it("loads models from its wrapped model service", function () {
|
||||
decorator.getModels(['a', 'b']).then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith(testModels);
|
||||
});
|
||||
|
||||
it("does not try to reload cached models", function () {
|
||||
mockModelService.getModels.andReturn(asPromise({ a: testModels.a }));
|
||||
decorator.getModels(['a']);
|
||||
mockModelService.getModels.andReturn(asPromise(testModels));
|
||||
decorator.getModels(['a', 'b']);
|
||||
expect(mockModelService.getModels).not.toHaveBeenCalledWith(['a', 'b']);
|
||||
expect(mockModelService.getModels.mostRecentCall.args[0]).toEqual(['b']);
|
||||
});
|
||||
|
||||
it("does not call its wrapped model service if not needed", function () {
|
||||
decorator.getModels(['a', 'b']);
|
||||
expect(mockModelService.getModels.calls.length).toEqual(1);
|
||||
decorator.getModels(['a', 'b']).then(mockCallback);
|
||||
expect(mockModelService.getModels.calls.length).toEqual(1);
|
||||
// Verify that we still got back our models, even though
|
||||
// no new call to the wrapped service was made
|
||||
expect(mockCallback).toHaveBeenCalledWith(testModels);
|
||||
});
|
||||
|
||||
it("ensures a single object instance, even for multiple concurrent calls", function () {
|
||||
var promiseA, promiseB, mockCallback = jasmine.createSpy();
|
||||
|
||||
promiseA = fakePromise();
|
||||
promiseB = fakePromise();
|
||||
|
||||
// Issue two calls before those promises resolve
|
||||
mockModelService.getModels.andReturn(promiseA);
|
||||
decorator.getModels(['a']);
|
||||
mockModelService.getModels.andReturn(promiseB);
|
||||
decorator.getModels(['a']).then(mockCallback);
|
||||
|
||||
// Then resolve those promises. Note that we're whiteboxing here
|
||||
// to figure out which promises to resolve (that is, we know that
|
||||
// two thens are chained after each getModels)
|
||||
promiseA.resolve(testModels);
|
||||
promiseB.resolve({
|
||||
a: { someNewKey: "some other value" }
|
||||
});
|
||||
|
||||
// Ensure that we have a pointer-identical instance
|
||||
expect(mockCallback.mostRecentCall.args[0].a)
|
||||
.toEqual({ someNewKey: "some other value" });
|
||||
expect(mockCallback.mostRecentCall.args[0].a)
|
||||
.toBe(testModels.a);
|
||||
});
|
||||
|
||||
it("is robust against updating with undefined values", function () {
|
||||
var promiseA, promiseB, mockCallback = jasmine.createSpy();
|
||||
|
||||
promiseA = fakePromise();
|
||||
promiseB = fakePromise();
|
||||
|
||||
// Issue two calls before those promises resolve
|
||||
mockModelService.getModels.andReturn(promiseA);
|
||||
decorator.getModels(['a']);
|
||||
mockModelService.getModels.andReturn(promiseB);
|
||||
decorator.getModels(['a']).then(mockCallback);
|
||||
|
||||
// Some model providers might erroneously add undefined values
|
||||
// under requested keys, so handle that
|
||||
promiseA.resolve({
|
||||
a: undefined
|
||||
});
|
||||
promiseB.resolve({
|
||||
a: { someNewKey: "some other value" }
|
||||
});
|
||||
|
||||
// Should still have gotten the model
|
||||
expect(mockCallback.mostRecentCall.args[0].a)
|
||||
.toEqual({ someNewKey: "some other value" });
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -12,8 +12,8 @@ define(
|
||||
var mockQ,
|
||||
mockProviders,
|
||||
modelList = [
|
||||
{ "a": { someKey: "some value" } },
|
||||
{ "b": { someOtherKey: "some other value" } }
|
||||
{ "a": { someKey: "some value" }, "b": undefined },
|
||||
{ "b": { someOtherKey: "some other value" }, "a": undefined }
|
||||
],
|
||||
aggregator;
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
"models/PersistedModelProvider",
|
||||
"models/RootModelProvider",
|
||||
"models/StaticModelProvider",
|
||||
"models/CachingModelDecorator",
|
||||
|
||||
"objects/DomainObject",
|
||||
"objects/DomainObjectProvider",
|
||||
|
@ -148,8 +148,11 @@ define(
|
||||
* failure of this request
|
||||
*/
|
||||
updateObject: function (space, key, value) {
|
||||
addToCache(space, key, value);
|
||||
return persistenceService.updateObject(space, key, value);
|
||||
return persistenceService.updateObject(space, key, value)
|
||||
.then(function (result) {
|
||||
addToCache(space, key, value);
|
||||
return result;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Delete an object in a specific space. This will
|
||||
|
2
platform/persistence/elastic/README.md
Normal file
2
platform/persistence/elastic/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
This bundle implements a connection to an external ElasticSearch persistence
|
||||
store in Open MCT Web.
|
43
platform/persistence/elastic/bundle.json
Normal file
43
platform/persistence/elastic/bundle.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "Couch Persistence",
|
||||
"description": "Adapter to read and write objects using a CouchDB instance.",
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"provides": "persistenceService",
|
||||
"type": "provider",
|
||||
"implementation": "ElasticPersistenceProvider.js",
|
||||
"depends": [ "$http", "$q", "PERSISTENCE_SPACE", "ELASTIC_ROOT", "ELASTIC_PATH" ]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "PERSISTENCE_SPACE",
|
||||
"value": "mct"
|
||||
},
|
||||
{
|
||||
"key": "ELASTIC_ROOT",
|
||||
"value": "/elastic"
|
||||
},
|
||||
{
|
||||
"key": "ELASTIC_PATH",
|
||||
"value": "mct/domain_object"
|
||||
},
|
||||
{
|
||||
"key": "ELASTIC_INDICATOR_INTERVAL",
|
||||
"value": 15000
|
||||
}
|
||||
],
|
||||
"indicators": [
|
||||
{
|
||||
"implementation": "ElasticIndicator.js",
|
||||
"depends": [
|
||||
"$http",
|
||||
"$interval",
|
||||
"ELASTIC_ROOT",
|
||||
"ELASTIC_INDICATOR_INTERVAL"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
95
platform/persistence/elastic/src/ElasticIndicator.js
Normal file
95
platform/persistence/elastic/src/ElasticIndicator.js
Normal file
@ -0,0 +1,95 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
// Set of connection states; changing among these states will be
|
||||
// reflected in the indicator's appearance.
|
||||
// CONNECTED: Everything nominal, expect to be able to read/write.
|
||||
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
|
||||
// PENDING: Still trying to connect, and haven't failed yet.
|
||||
var CONNECTED = {
|
||||
text: "Connected",
|
||||
glyphClass: "ok",
|
||||
description: "Connected to the domain object database."
|
||||
},
|
||||
DISCONNECTED = {
|
||||
text: "Disconnected",
|
||||
glyphClass: "err",
|
||||
description: "Unable to connect to the domain object database."
|
||||
},
|
||||
PENDING = {
|
||||
text: "Checking connection..."
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicator for the current CouchDB connection. Polls CouchDB
|
||||
* at a regular interval (defined by bundle constants) to ensure
|
||||
* that the database is available.
|
||||
*/
|
||||
function ElasticIndicator($http, $interval, PATH, INTERVAL) {
|
||||
// Track the current connection state
|
||||
var state = PENDING;
|
||||
|
||||
// Callback if the HTTP request to Couch fails
|
||||
function handleError(err) {
|
||||
state = DISCONNECTED;
|
||||
}
|
||||
|
||||
// Callback if the HTTP request succeeds.
|
||||
function handleResponse(response) {
|
||||
state = CONNECTED;
|
||||
}
|
||||
|
||||
// Try to connect to CouchDB, and update the indicator.
|
||||
function updateIndicator() {
|
||||
$http.get(PATH).then(handleResponse, handleError);
|
||||
}
|
||||
|
||||
// Update the indicator initially, and start polling.
|
||||
updateIndicator();
|
||||
$interval(updateIndicator, INTERVAL, false);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the glyph (single character used as an icon)
|
||||
* to display in this indicator. This will return "D",
|
||||
* which should appear as a database icon.
|
||||
* @returns {string} the character of the database icon
|
||||
*/
|
||||
getGlyph: function () {
|
||||
return "D";
|
||||
},
|
||||
/**
|
||||
* Get the name of the CSS class to apply to the glyph.
|
||||
* This is used to color the glyph to match its
|
||||
* state (one of ok, caution or err)
|
||||
* @returns {string} the CSS class to apply to this glyph
|
||||
*/
|
||||
getGlyphClass: function () {
|
||||
return state.glyphClass;
|
||||
},
|
||||
/**
|
||||
* Get the text that should appear in the indicator.
|
||||
* @returns {string} brief summary of connection status
|
||||
*/
|
||||
getText: function () {
|
||||
return state.text;
|
||||
},
|
||||
/**
|
||||
* Get a longer-form description of the current connection
|
||||
* space, suitable for display in a tooltip
|
||||
* @returns {string} longer summary of connection status
|
||||
*/
|
||||
getDescription: function () {
|
||||
return state.description;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return ElasticIndicator;
|
||||
}
|
||||
);
|
179
platform/persistence/elastic/src/ElasticPersistenceProvider.js
Normal file
179
platform/persistence/elastic/src/ElasticPersistenceProvider.js
Normal file
@ -0,0 +1,179 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
// JSLint doesn't like underscore-prefixed properties,
|
||||
// so hide them here.
|
||||
var SRC = "_source",
|
||||
REV = "_version",
|
||||
ID = "_id",
|
||||
CONFLICT = 409;
|
||||
|
||||
/**
|
||||
* The ElasticPersistenceProvider reads and writes JSON documents
|
||||
* (more specifically, domain object models) to/from an ElasticSearch
|
||||
* instance.
|
||||
* @constructor
|
||||
*/
|
||||
function ElasticPersistenceProvider($http, $q, SPACE, ROOT, PATH) {
|
||||
var spaces = [ SPACE ],
|
||||
revs = {};
|
||||
|
||||
// Convert a subpath to a full path, suitable to pass
|
||||
// to $http.
|
||||
function url(subpath) {
|
||||
return ROOT + '/' + PATH + '/' + subpath;
|
||||
}
|
||||
|
||||
// Issue a request using $http; get back the plain JS object
|
||||
// from the expected JSON response
|
||||
function request(subpath, method, value, params) {
|
||||
return $http({
|
||||
method: method,
|
||||
url: url(subpath),
|
||||
params: params,
|
||||
data: value
|
||||
}).then(function (response) {
|
||||
return response.data;
|
||||
}, function (response) {
|
||||
return (response || {}).data;
|
||||
});
|
||||
}
|
||||
|
||||
// Shorthand methods for GET/PUT methods
|
||||
function get(subpath) {
|
||||
return request(subpath, "GET");
|
||||
}
|
||||
function put(subpath, value, params) {
|
||||
return request(subpath, "PUT", value, params);
|
||||
}
|
||||
function del(subpath) {
|
||||
return request(subpath, "DELETE");
|
||||
}
|
||||
|
||||
// Get a domain object model out of CouchDB's response
|
||||
function getModel(response) {
|
||||
if (response && response[SRC]) {
|
||||
revs[response[ID]] = response[REV];
|
||||
return response[SRC];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle an update error
|
||||
function handleError(response, key) {
|
||||
var error = new Error("Persistence error.");
|
||||
if ((response || {}).status === CONFLICT) {
|
||||
error.key = "revision";
|
||||
// Load the updated model, then reject the promise
|
||||
return get(key).then(function (response) {
|
||||
error.model = response[SRC];
|
||||
return $q.reject(error);
|
||||
});
|
||||
}
|
||||
// Reject the promise
|
||||
return $q.reject(error);
|
||||
}
|
||||
|
||||
// Check the response to a create/update/delete request;
|
||||
// track the rev if it's valid, otherwise return false to
|
||||
// indicate that the request failed.
|
||||
function checkResponse(response, key) {
|
||||
var error;
|
||||
if (response && !response.error) {
|
||||
revs[key] = response[REV];
|
||||
return response;
|
||||
} else {
|
||||
return handleError(response, key);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* List all persistence spaces which this provider
|
||||
* recognizes.
|
||||
*
|
||||
* @returns {Promise.<string[]>} a promise for a list of
|
||||
* spaces supported by this provider
|
||||
*/
|
||||
listSpaces: function () {
|
||||
return $q.when(spaces);
|
||||
},
|
||||
/**
|
||||
* List all objects (by their identifiers) that are stored
|
||||
* in the given persistence space, per this provider.
|
||||
* @param {string} space the space to check
|
||||
* @returns {Promise.<string[]>} a promise for the list of
|
||||
* identifiers
|
||||
*/
|
||||
listObjects: function (space) {
|
||||
return $q.when([]);
|
||||
},
|
||||
/**
|
||||
* Create a new object in the specified persistence space.
|
||||
* @param {string} space the space in which to store the object
|
||||
* @param {string} key the identifier for the persisted object
|
||||
* @param {object} value a JSONifiable object that should be
|
||||
* stored and associated with the provided identifier
|
||||
* @returns {Promise.<boolean>} a promise for an indication
|
||||
* of the success (true) or failure (false) of this
|
||||
* operation
|
||||
*/
|
||||
createObject: function (space, key, value) {
|
||||
return put(key, value).then(checkResponse);
|
||||
},
|
||||
|
||||
/**
|
||||
* Read an existing object back from persistence.
|
||||
* @param {string} space the space in which to look for
|
||||
* the object
|
||||
* @param {string} key the identifier for the persisted object
|
||||
* @returns {Promise.<object>} a promise for the stored
|
||||
* object; this will resolve to undefined if no such
|
||||
* object is found.
|
||||
*/
|
||||
readObject: function (space, key) {
|
||||
return get(key).then(getModel);
|
||||
},
|
||||
/**
|
||||
* Update an existing object in the specified persistence space.
|
||||
* @param {string} space the space in which to store the object
|
||||
* @param {string} key the identifier for the persisted object
|
||||
* @param {object} value a JSONifiable object that should be
|
||||
* stored and associated with the provided identifier
|
||||
* @returns {Promise.<boolean>} a promise for an indication
|
||||
* of the success (true) or failure (false) of this
|
||||
* operation
|
||||
*/
|
||||
updateObject: function (space, key, value) {
|
||||
function checkUpdate(response) {
|
||||
return checkResponse(response, key);
|
||||
}
|
||||
return put(key, value, { version: revs[key] })
|
||||
.then(checkUpdate);
|
||||
},
|
||||
/**
|
||||
* Delete an object in the specified persistence space.
|
||||
* @param {string} space the space from which to delete this
|
||||
* object
|
||||
* @param {string} key the identifier of the persisted object
|
||||
* @param {object} value a JSONifiable object that should be
|
||||
* deleted
|
||||
* @returns {Promise.<boolean>} a promise for an indication
|
||||
* of the success (true) or failure (false) of this
|
||||
* operation
|
||||
*/
|
||||
deleteObject: function (space, key, value) {
|
||||
return del(key).then(checkResponse);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return ElasticPersistenceProvider;
|
||||
}
|
||||
);
|
90
platform/persistence/elastic/test/ElasticIndicatorSpec.js
Normal file
90
platform/persistence/elastic/test/ElasticIndicatorSpec.js
Normal file
@ -0,0 +1,90 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/ElasticIndicator"],
|
||||
function (ElasticIndicator) {
|
||||
"use strict";
|
||||
|
||||
describe("The ElasticSearch status indicator", function () {
|
||||
var mockHttp,
|
||||
mockInterval,
|
||||
testPath,
|
||||
testInterval,
|
||||
mockPromise,
|
||||
indicator;
|
||||
|
||||
beforeEach(function () {
|
||||
mockHttp = jasmine.createSpyObj("$http", [ "get" ]);
|
||||
mockInterval = jasmine.createSpy("$interval");
|
||||
mockPromise = jasmine.createSpyObj("promise", [ "then" ]);
|
||||
testPath = "/test/path";
|
||||
testInterval = 12321; // Some number
|
||||
|
||||
mockHttp.get.andReturn(mockPromise);
|
||||
|
||||
indicator = new ElasticIndicator(
|
||||
mockHttp,
|
||||
mockInterval,
|
||||
testPath,
|
||||
testInterval
|
||||
);
|
||||
});
|
||||
|
||||
it("polls for changes", function () {
|
||||
expect(mockInterval).toHaveBeenCalledWith(
|
||||
jasmine.any(Function),
|
||||
testInterval,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("has a database icon", function () {
|
||||
expect(indicator.getGlyph()).toEqual("D");
|
||||
});
|
||||
|
||||
it("consults the database at the configured path", function () {
|
||||
expect(mockHttp.get).toHaveBeenCalledWith(testPath);
|
||||
});
|
||||
|
||||
it("changes when the database connection is nominal", function () {
|
||||
var initialText = indicator.getText(),
|
||||
initialDescrption = indicator.getDescription(),
|
||||
initialGlyphClass = indicator.getGlyphClass();
|
||||
|
||||
// Nominal just means getting back an objeect, without
|
||||
// an error field.
|
||||
mockPromise.then.mostRecentCall.args[0]({ data: {} });
|
||||
|
||||
// Verify that these values changed;
|
||||
// don't test for specific text.
|
||||
expect(indicator.getText()).not.toEqual(initialText);
|
||||
expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
|
||||
expect(indicator.getDescription()).not.toEqual(initialDescrption);
|
||||
|
||||
// Do check for specific class
|
||||
expect(indicator.getGlyphClass()).toEqual("ok");
|
||||
});
|
||||
|
||||
it("changes when the server cannot be reached", function () {
|
||||
var initialText = indicator.getText(),
|
||||
initialDescrption = indicator.getDescription(),
|
||||
initialGlyphClass = indicator.getGlyphClass();
|
||||
|
||||
// Nominal just means getting back an objeect, without
|
||||
// an error field.
|
||||
mockPromise.then.mostRecentCall.args[1]({ data: {} });
|
||||
|
||||
// Verify that these values changed;
|
||||
// don't test for specific text.
|
||||
expect(indicator.getText()).not.toEqual(initialText);
|
||||
expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
|
||||
expect(indicator.getDescription()).not.toEqual(initialDescrption);
|
||||
|
||||
// Do check for specific class
|
||||
expect(indicator.getGlyphClass()).toEqual("err");
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,195 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/ElasticPersistenceProvider"],
|
||||
function (ElasticPersistenceProvider) {
|
||||
"use strict";
|
||||
|
||||
describe("The ElasticSearch persistence provider", function () {
|
||||
var mockHttp,
|
||||
mockQ,
|
||||
testSpace = "testSpace",
|
||||
testRoot = "/test",
|
||||
testPath = "db",
|
||||
capture,
|
||||
provider;
|
||||
|
||||
function mockPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockHttp = jasmine.createSpy("$http");
|
||||
mockQ = jasmine.createSpyObj("$q", ["when", "reject"]);
|
||||
|
||||
mockQ.when.andCallFake(mockPromise);
|
||||
mockQ.reject.andCallFake(function (value) {
|
||||
return {
|
||||
then: function (ignored, callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Capture promise results
|
||||
capture = jasmine.createSpy("capture");
|
||||
|
||||
provider = new ElasticPersistenceProvider(
|
||||
mockHttp,
|
||||
mockQ,
|
||||
testSpace,
|
||||
testRoot,
|
||||
testPath
|
||||
);
|
||||
});
|
||||
|
||||
it("reports available spaces", function () {
|
||||
provider.listSpaces().then(capture);
|
||||
expect(capture).toHaveBeenCalledWith([testSpace]);
|
||||
});
|
||||
|
||||
// General pattern of tests below is to simulate ElasticSearch's
|
||||
// response, verify that request looks like what ElasticSearch
|
||||
// would expect, and finally verify that ElasticPersistenceProvider's
|
||||
// return values match what is expected.
|
||||
it("lists all available documents", function () {
|
||||
// Not implemented yet
|
||||
provider.listObjects().then(capture);
|
||||
expect(capture).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it("allows object creation", function () {
|
||||
var model = { someKey: "some value" };
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 1 }
|
||||
}));
|
||||
provider.createObject("testSpace", "abc", model).then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
url: "/test/db/abc",
|
||||
method: "PUT",
|
||||
data: model
|
||||
});
|
||||
expect(capture.mostRecentCall.args[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
it("allows object models to be read back", function () {
|
||||
var model = { someKey: "some value" };
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 1, "_source": model }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc").then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
url: "/test/db/abc",
|
||||
method: "GET"
|
||||
});
|
||||
expect(capture).toHaveBeenCalledWith(model);
|
||||
});
|
||||
|
||||
it("allows object update", function () {
|
||||
var model = { someKey: "some value" };
|
||||
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
// Now perform an update
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 43, "_source": {} }
|
||||
}));
|
||||
provider.updateObject("testSpace", "abc", model).then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
url: "/test/db/abc",
|
||||
method: "PUT",
|
||||
params: { version: 42 },
|
||||
data: model
|
||||
});
|
||||
expect(capture.mostRecentCall.args[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
it("allows object deletion", function () {
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
// Now perform an update
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
}));
|
||||
provider.deleteObject("testSpace", "abc", {}).then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
url: "/test/db/abc",
|
||||
method: "DELETE"
|
||||
});
|
||||
expect(capture.mostRecentCall.args[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns undefined when objects are not found", function () {
|
||||
// Act like a 404
|
||||
mockHttp.andReturn({
|
||||
then: function (success, fail) {
|
||||
return mockPromise(fail());
|
||||
}
|
||||
});
|
||||
provider.readObject("testSpace", "abc").then(capture);
|
||||
expect(capture).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("handles rejection due to version", function () {
|
||||
var model = { someKey: "some value" },
|
||||
mockErrorCallback = jasmine.createSpy('error');
|
||||
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
// Now perform an update
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "status": 409, "error": "Revision error..." }
|
||||
}));
|
||||
provider.updateObject("testSpace", "abc", model).then(
|
||||
capture,
|
||||
mockErrorCallback
|
||||
);
|
||||
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
expect(mockErrorCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles rejection due to unknown reasons", function () {
|
||||
var model = { someKey: "some value" },
|
||||
mockErrorCallback = jasmine.createSpy('error');
|
||||
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
// Now perform an update
|
||||
mockHttp.andReturn(mockPromise({
|
||||
data: { "status": 410, "error": "Revision error..." }
|
||||
}));
|
||||
provider.updateObject("testSpace", "abc", model).then(
|
||||
capture,
|
||||
mockErrorCallback
|
||||
);
|
||||
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
expect(mockErrorCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
4
platform/persistence/elastic/test/suite.json
Normal file
4
platform/persistence/elastic/test/suite.json
Normal file
@ -0,0 +1,4 @@
|
||||
[
|
||||
"ElasticIndicator",
|
||||
"ElasticPersistenceProvider"
|
||||
]
|
5
platform/persistence/queue/README.md
Normal file
5
platform/persistence/queue/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
This bundle provides an Overwrite/Cancel dialog when persisting
|
||||
domain objects, if persistence fails. It is meant to be paired
|
||||
with a persistence adapter which performs revision-checking
|
||||
on update calls, in order to provide the user interface for
|
||||
choosing between Overwrite and Cancel in that situation.
|
42
platform/persistence/queue/bundle.json
Normal file
42
platform/persistence/queue/bundle.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"provides": "capabilityService",
|
||||
"implementation": "QueuingPersistenceCapabilityDecorator.js",
|
||||
"depends": [ "persistenceQueue" ]
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "persistenceQueue",
|
||||
"implementation": "PersistenceQueue.js",
|
||||
"depends": [
|
||||
"$q",
|
||||
"$timeout",
|
||||
"dialogService",
|
||||
"PERSISTENCE_QUEUE_DELAY"
|
||||
]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "PERSISTENCE_QUEUE_DELAY",
|
||||
"value": 5
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
{
|
||||
"key": "persistence-failure-dialog",
|
||||
"templateUrl": "templates/persistence-failure-dialog.html"
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "PersistenceFailureController",
|
||||
"implementation": "PersistenceFailureController.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<span ng-controller="PersistenceFailureController as controller">
|
||||
|
||||
<div ng-if="ngModel.revised.length > 0">
|
||||
External changes have been made to the following objects:
|
||||
<ul>
|
||||
<li ng-repeat="failure in ngModel.revised">
|
||||
<mct-representation key="'label'"
|
||||
mct-object="failure.domainObject">
|
||||
</mct-representation>
|
||||
was modified at
|
||||
<b>{{controller.formatTimestamp(failure.error.model.modified)}}</b>
|
||||
by
|
||||
<i>{{controller.formatUsername(failure.error.model.modifier)}}</i>
|
||||
</li>
|
||||
</ul>
|
||||
You may overwrite these objects, or discard your changes to keep
|
||||
the updates that were made externally.
|
||||
</div>
|
||||
|
||||
<div ng-if="ngModel.unrecoverable.length > 0">
|
||||
Changes to these objects could not be saved for unknown reasons:
|
||||
<ul>
|
||||
<li ng-repeat="failure in ngModel.unrecoverable">
|
||||
<mct-representation key="'label'"
|
||||
mct-object="failure.domainObject">
|
||||
</mct-representation>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</span>
|
@ -0,0 +1,8 @@
|
||||
/*global define*/
|
||||
|
||||
define({
|
||||
REVISION_ERROR_KEY: "revision",
|
||||
OVERWRITE_KEY: "overwrite",
|
||||
TIMESTAMP_FORMAT: "YYYY-MM-DD HH:mm:ss\\Z",
|
||||
UNKNOWN_USER: "unknown user"
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['moment', './PersistenceFailureConstants'],
|
||||
function (moment, Constants) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Controller to support the template to be shown in the
|
||||
* dialog shown for persistence failures.
|
||||
*/
|
||||
function PersistenceFailureController() {
|
||||
return {
|
||||
/**
|
||||
* Format a timestamp for display in the dialog.
|
||||
*/
|
||||
formatTimestamp: function (timestamp) {
|
||||
return moment.utc(timestamp)
|
||||
.format(Constants.TIMESTAMP_FORMAT);
|
||||
},
|
||||
/**
|
||||
* Format a user name for display in the dialog.
|
||||
*/
|
||||
formatUsername: function (username) {
|
||||
return username || Constants.UNKNOWN_USER;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return PersistenceFailureController;
|
||||
}
|
||||
);
|
54
platform/persistence/queue/src/PersistenceFailureDialog.js
Normal file
54
platform/persistence/queue/src/PersistenceFailureDialog.js
Normal file
@ -0,0 +1,54 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./PersistenceFailureConstants'],
|
||||
function (PersistenceFailureConstants) {
|
||||
"use strict";
|
||||
|
||||
var OVERWRITE_CANCEL_OPTIONS = [
|
||||
{
|
||||
name: "Overwrite",
|
||||
key: PersistenceFailureConstants.OVERWRITE_KEY
|
||||
},
|
||||
{
|
||||
name: "Discard",
|
||||
key: "cancel"
|
||||
}
|
||||
],
|
||||
OK_OPTIONS = [ { name: "OK", key: "ok" } ];
|
||||
|
||||
/**
|
||||
* Populates a `dialogModel` to pass to `dialogService.getUserChoise`
|
||||
* in order to choose between Overwrite and Cancel.
|
||||
*/
|
||||
function PersistenceFailureDialog(failures) {
|
||||
var revisionErrors = [],
|
||||
otherErrors = [];
|
||||
|
||||
// Place this failure into an appropriate group
|
||||
function categorizeFailure(failure) {
|
||||
// Check if the error is due to object revision
|
||||
var isRevisionError = ((failure || {}).error || {}).key ===
|
||||
PersistenceFailureConstants.REVISION_ERROR_KEY;
|
||||
// Push the failure into the appropriate group
|
||||
(isRevisionError ? revisionErrors : otherErrors).push(failure);
|
||||
}
|
||||
|
||||
// Separate into revision errors, and other errors
|
||||
failures.forEach(categorizeFailure);
|
||||
|
||||
return {
|
||||
title: "Save Error",
|
||||
template: "persistence-failure-dialog",
|
||||
model: {
|
||||
revised: revisionErrors,
|
||||
unrecoverable: otherErrors
|
||||
},
|
||||
options: revisionErrors.length > 0 ?
|
||||
OVERWRITE_CANCEL_OPTIONS : OK_OPTIONS
|
||||
};
|
||||
}
|
||||
|
||||
return PersistenceFailureDialog;
|
||||
}
|
||||
);
|
110
platform/persistence/queue/src/PersistenceFailureHandler.js
Normal file
110
platform/persistence/queue/src/PersistenceFailureHandler.js
Normal file
@ -0,0 +1,110 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./PersistenceFailureDialog', './PersistenceFailureConstants'],
|
||||
function (PersistenceFailureDialog, PersistenceFailureConstants) {
|
||||
"use strict";
|
||||
|
||||
function PersistenceFailureHandler($q, dialogService) {
|
||||
// Refresh revision information for the domain object associated
|
||||
// with this persistence failure
|
||||
function refresh(failure) {
|
||||
// Refresh the domain object to the latest from persistence
|
||||
return failure.persistence.refresh();
|
||||
}
|
||||
|
||||
// Issue a new persist call for the domain object associated with
|
||||
// this failure.
|
||||
function persist(failure) {
|
||||
// Note that we reissue the persist request here, but don't
|
||||
// return it, to avoid a circular wait. We trust that the
|
||||
// PersistenceQueue will behave correctly on the next round
|
||||
// of flushing.
|
||||
failure.requeue();
|
||||
}
|
||||
|
||||
// Retry persistence (overwrite) for this set of failed attempts
|
||||
function retry(failures) {
|
||||
var models = {};
|
||||
|
||||
// Cache a copy of the model
|
||||
function cacheModel(failure) {
|
||||
// Clone...
|
||||
models[failure.id] = JSON.parse(JSON.stringify(
|
||||
failure.domainObject.getModel()
|
||||
));
|
||||
}
|
||||
|
||||
// Mutate a domain object to restore its model
|
||||
function remutate(failure) {
|
||||
var model = models[failure.id];
|
||||
return failure.domainObject.useCapability(
|
||||
"mutation",
|
||||
function () { return model; },
|
||||
model.modified
|
||||
);
|
||||
}
|
||||
|
||||
// Cache the object models we might want to save
|
||||
failures.forEach(cacheModel);
|
||||
|
||||
// Strategy here:
|
||||
// * Cache all of the models we might want to save (above)
|
||||
// * Refresh all domain objects (so they are latest versions)
|
||||
// * Re-insert the cached domain object models
|
||||
// * Invoke persistence again
|
||||
return $q.all(failures.map(refresh)).then(function () {
|
||||
return $q.all(failures.map(remutate));
|
||||
}).then(function () {
|
||||
return $q.all(failures.map(persist));
|
||||
});
|
||||
}
|
||||
|
||||
// Discard changes for a failed refresh
|
||||
function discard(failure) {
|
||||
var persistence =
|
||||
failure.domainObject.getCapability('persistence');
|
||||
return persistence.refresh();
|
||||
}
|
||||
|
||||
// Discard changes associated with a failed save
|
||||
function discardAll(failures) {
|
||||
return $q.all(failures.map(discard));
|
||||
}
|
||||
|
||||
// Handle failures in persistence
|
||||
function handleFailures(failures) {
|
||||
// Prepare dialog for display
|
||||
var dialogModel = new PersistenceFailureDialog(failures),
|
||||
revisionErrors = dialogModel.model.revised;
|
||||
|
||||
// Handle user input (did they choose to overwrite?)
|
||||
function handleChoice(key) {
|
||||
// If so, try again
|
||||
if (key === PersistenceFailureConstants.OVERWRITE_KEY) {
|
||||
return retry(revisionErrors);
|
||||
} else {
|
||||
return discardAll(revisionErrors);
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for user input, the overwrite if they said so.
|
||||
return dialogService.getUserChoice(dialogModel)
|
||||
.then(handleChoice, handleChoice);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Handle persistence failures by providing the user with a
|
||||
* dialog summarizing these failures, and giving the option
|
||||
* to overwrite/cancel as appropriate.
|
||||
* @param {Array} failures persistence failures, as prepared
|
||||
* by PersistenceQueueHandler
|
||||
*/
|
||||
handle: handleFailures
|
||||
};
|
||||
}
|
||||
|
||||
return PersistenceFailureHandler;
|
||||
}
|
||||
);
|
56
platform/persistence/queue/src/PersistenceQueue.js
Normal file
56
platform/persistence/queue/src/PersistenceQueue.js
Normal file
@ -0,0 +1,56 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[
|
||||
'./PersistenceQueueImpl',
|
||||
'./PersistenceQueueHandler',
|
||||
'./PersistenceFailureHandler'
|
||||
],
|
||||
function (
|
||||
PersistenceQueueImpl,
|
||||
PersistenceQueueHandler,
|
||||
PersistenceFailureHandler
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* The PersistenceQueue is used by the QueuingPersistenceCapability
|
||||
* to aggregrate calls for object persistence. These are then issued
|
||||
* in a group, such that if some or all are rejected, this result can
|
||||
* be shown to the user (again, in a group.)
|
||||
*
|
||||
* This constructor is exposed as a service, but handles only the
|
||||
* wiring of injected dependencies; behavior is implemented in the
|
||||
* various component parts.
|
||||
*
|
||||
* @param $timeout Angular's $timeout
|
||||
* @param {PersistenceQueueHandler} handler handles actual
|
||||
* persistence when the queue is flushed
|
||||
* @param {number} [DELAY] optional; delay in milliseconds between
|
||||
* attempts to flush the queue
|
||||
*/
|
||||
function PersistenceQueue(
|
||||
$q,
|
||||
$timeout,
|
||||
dialogService,
|
||||
PERSISTENCE_QUEUE_DELAY
|
||||
) {
|
||||
// Wire up injected dependencies
|
||||
return new PersistenceQueueImpl(
|
||||
$q,
|
||||
$timeout,
|
||||
new PersistenceQueueHandler(
|
||||
$q,
|
||||
new PersistenceFailureHandler(
|
||||
$q,
|
||||
dialogService
|
||||
)
|
||||
),
|
||||
PERSISTENCE_QUEUE_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
return PersistenceQueue;
|
||||
}
|
||||
);
|
90
platform/persistence/queue/src/PersistenceQueueHandler.js
Normal file
90
platform/persistence/queue/src/PersistenceQueueHandler.js
Normal file
@ -0,0 +1,90 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Handles actual persistence invocations for queeud persistence
|
||||
* attempts, in a group. Handling in a group in this manner
|
||||
* also allows failure to be handled in a group (e.g. in a single
|
||||
* summary dialog.)
|
||||
* @param $q Angular's $q, for promises
|
||||
* @param {PersistenceFailureHandler} handler to invoke in the event
|
||||
* that a persistence attempt fails.
|
||||
*/
|
||||
function PersistenceQueueHandler($q, failureHandler) {
|
||||
|
||||
// Handle a group of persistence invocations
|
||||
function persistGroup(ids, persistences, domainObjects, queue) {
|
||||
var failures = [];
|
||||
|
||||
// Try to persist a specific domain object
|
||||
function tryPersist(id) {
|
||||
// Look up its persistence capability from the provided
|
||||
// id->persistence object.
|
||||
var persistence = persistences[id],
|
||||
domainObject = domainObjects[id];
|
||||
|
||||
// Put a domain object back in the queue
|
||||
// (e.g. after Overwrite)
|
||||
function requeue() {
|
||||
return queue.put(domainObject, persistence);
|
||||
}
|
||||
|
||||
// Handle success
|
||||
function succeed(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Handle failure (build up a list of failures)
|
||||
function fail(error) {
|
||||
failures.push({
|
||||
id: id,
|
||||
persistence: persistence,
|
||||
domainObject: domainObject,
|
||||
requeue: requeue,
|
||||
error: error
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invoke the actual persistence capability, then
|
||||
// note success or failures
|
||||
return persistence.persist().then(succeed, fail);
|
||||
}
|
||||
|
||||
// Handle any failures from the full operation
|
||||
function handleFailure(value) {
|
||||
return failures.length > 0 ?
|
||||
failureHandler.handle(failures) :
|
||||
value;
|
||||
}
|
||||
|
||||
// Try to persist everything, then handle any failures
|
||||
return $q.all(ids.map(tryPersist)).then(handleFailure);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
/**
|
||||
* Invoke the persist method on the provided persistence
|
||||
* capabilities.
|
||||
* @param {Object.<string,PersistenceCapability>} persistences
|
||||
* capabilities to invoke, in id->capability pairs.
|
||||
* @param {Object.<string,DomainObject>} domainObjects
|
||||
* associated domain objects, in id->object pairs.
|
||||
* @param {PersistenceQueue} queue the persistence queue,
|
||||
* to requeue as necessary
|
||||
*/
|
||||
persist: function (persistences, domainObjects, queue) {
|
||||
var ids = Object.keys(persistences);
|
||||
return persistGroup(ids, persistences, domainObjects, queue);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return PersistenceQueueHandler;
|
||||
}
|
||||
);
|
114
platform/persistence/queue/src/PersistenceQueueImpl.js
Normal file
114
platform/persistence/queue/src/PersistenceQueueImpl.js
Normal file
@ -0,0 +1,114 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The PersistenceQueue is used by the QueuingPersistenceCapability
|
||||
* to aggregrate calls for object persistence. These are then issued
|
||||
* in a group, such that if some or all are rejected, this result can
|
||||
* be shown to the user (again, in a group.)
|
||||
*
|
||||
* This implementation is separate out from PersistenceQueue, which
|
||||
* handles the wiring of injected dependencies into an instance of
|
||||
* this class.
|
||||
*
|
||||
* @param $timeout Angular's $timeout
|
||||
* @param {PersistenceQueueHandler} handler handles actual
|
||||
* persistence when the queue is flushed
|
||||
* @param {number} [DELAY] optional; delay in milliseconds between
|
||||
* attempts to flush the queue
|
||||
*/
|
||||
function PersistenceQueueImpl($q, $timeout, handler, DELAY) {
|
||||
var self,
|
||||
persistences = {},
|
||||
objects = {},
|
||||
lastObservedSize = 0,
|
||||
pendingTimeout,
|
||||
flushPromise,
|
||||
activeDefer = $q.defer();
|
||||
|
||||
// Check if the queue's size has stopped increasing)
|
||||
function quiescent() {
|
||||
return Object.keys(persistences).length === lastObservedSize;
|
||||
}
|
||||
|
||||
// Persist all queued objects
|
||||
function flush() {
|
||||
// Get a local reference to the active promise;
|
||||
// this will be replaced with a promise for the next round
|
||||
// of persistence calls, so we want to make sure we clear
|
||||
// the correct one when this flush completes.
|
||||
var flushingDefer = activeDefer;
|
||||
|
||||
// Clear the active promise for a queue flush
|
||||
function clearFlushPromise(value) {
|
||||
flushPromise = undefined;
|
||||
flushingDefer.resolve(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Persist all queued objects
|
||||
flushPromise = handler.persist(persistences, objects, self)
|
||||
.then(clearFlushPromise, clearFlushPromise);
|
||||
|
||||
// Reset queue, etc.
|
||||
persistences = {};
|
||||
objects = {};
|
||||
lastObservedSize = 0;
|
||||
pendingTimeout = undefined;
|
||||
activeDefer = $q.defer();
|
||||
}
|
||||
|
||||
// Schedule a flushing of the queue (that is, plan to flush
|
||||
// all objects in the queue)
|
||||
function scheduleFlush() {
|
||||
function maybeFlush() {
|
||||
// Timeout fired, so clear it
|
||||
pendingTimeout = undefined;
|
||||
// Only flush when we've stopped receiving updates
|
||||
(quiescent() ? flush : scheduleFlush)();
|
||||
// Update lastObservedSize to detect quiescence
|
||||
lastObservedSize = Object.keys(persistences).length;
|
||||
}
|
||||
|
||||
// If we are already flushing the queue...
|
||||
if (flushPromise) {
|
||||
// Wait until that's over before considering a flush
|
||||
flushPromise.then(maybeFlush);
|
||||
} else {
|
||||
// Otherwise, schedule a flush on a timeout (to give
|
||||
// a window for other updates to get aggregated)
|
||||
pendingTimeout = pendingTimeout ||
|
||||
$timeout(maybeFlush, DELAY, false);
|
||||
}
|
||||
|
||||
return activeDefer.promise;
|
||||
}
|
||||
|
||||
// If no delay is provided, use a default
|
||||
DELAY = DELAY || 0;
|
||||
|
||||
self = {
|
||||
/**
|
||||
* Queue persistence of a domain object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* @param {PersistenceCapability} persistence the object's
|
||||
* undecorated persistence capability
|
||||
*/
|
||||
put: function (domainObject, persistence) {
|
||||
var id = domainObject.getId();
|
||||
persistences[id] = persistence;
|
||||
objects[id] = domainObject;
|
||||
return scheduleFlush();
|
||||
}
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return PersistenceQueueImpl;
|
||||
}
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The QueuingPersistenceCapability places `persist` calls in a queue
|
||||
* to be handled in batches.
|
||||
* @param {PersistenceQueue} queue of persistence calls
|
||||
* @param {PersistenceCapability} persistence the wrapped persistence
|
||||
* capability
|
||||
* @param {DomainObject} domainObject the domain object which exposes
|
||||
* the capability
|
||||
*/
|
||||
function QueuingPersistenceCapability(queue, persistence, domainObject) {
|
||||
var queuingPersistence = Object.create(persistence);
|
||||
|
||||
// Override persist calls to queue them instead
|
||||
queuingPersistence.persist = function () {
|
||||
return queue.put(domainObject, persistence);
|
||||
};
|
||||
|
||||
return queuingPersistence;
|
||||
}
|
||||
|
||||
return QueuingPersistenceCapability;
|
||||
}
|
||||
);
|
@ -0,0 +1,76 @@
|
||||
/*global define,Promise*/
|
||||
|
||||
/**
|
||||
* Module defining CoreCapabilityProvider. Created by vwoeltje on 11/7/14.
|
||||
*/
|
||||
define(
|
||||
['./QueuingPersistenceCapability'],
|
||||
function (QueuingPersistenceCapability) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Capability decorator. Adds queueing support to persistence
|
||||
* capabilities for domain objects, such that persistence attempts
|
||||
* will be handled in batches (allowing failure notification to
|
||||
* also be presented in batches.)
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function QueuingPersistenceCapabilityDecorator(
|
||||
persistenceQueue,
|
||||
capabilityService
|
||||
) {
|
||||
|
||||
function decoratePersistence(capabilities) {
|
||||
var originalPersistence = capabilities.persistence;
|
||||
if (originalPersistence) {
|
||||
capabilities.persistence = function (domainObject) {
|
||||
// Get/instantiate the original
|
||||
var original =
|
||||
(typeof originalPersistence === 'function') ?
|
||||
originalPersistence(domainObject) :
|
||||
originalPersistence;
|
||||
|
||||
// Provide a decorated version
|
||||
return new QueuingPersistenceCapability(
|
||||
persistenceQueue,
|
||||
original,
|
||||
domainObject
|
||||
);
|
||||
};
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
function getCapabilities(model) {
|
||||
return decoratePersistence(
|
||||
capabilityService.getCapabilities(model)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get all capabilities associated with a given domain
|
||||
* object.
|
||||
*
|
||||
* This returns a promise for an object containing key-value
|
||||
* pairs, where keys are capability names and values are
|
||||
* either:
|
||||
*
|
||||
* * Capability instances
|
||||
* * Capability constructors (which take a domain object
|
||||
* as their argument.)
|
||||
*
|
||||
*
|
||||
* @param {*} model the object model
|
||||
* @returns {Object.<string,function|Capability>} all
|
||||
* capabilities known to be valid for this model, as
|
||||
* key-value pairs
|
||||
*/
|
||||
getCapabilities: getCapabilities
|
||||
};
|
||||
}
|
||||
|
||||
return QueuingPersistenceCapabilityDecorator;
|
||||
}
|
||||
);
|
@ -0,0 +1,16 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceFailureConstants"],
|
||||
function (PersistenceFailureConstants) {
|
||||
"use strict";
|
||||
|
||||
describe("Persistence failure constants", function () {
|
||||
it("defines an overwrite key", function () {
|
||||
expect(PersistenceFailureConstants.OVERWRITE_KEY)
|
||||
.toEqual(jasmine.any(String));
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,27 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceFailureController"],
|
||||
function (PersistenceFailureController) {
|
||||
"use strict";
|
||||
|
||||
describe("The persistence failure controller", function () {
|
||||
var controller;
|
||||
|
||||
beforeEach(function () {
|
||||
controller = new PersistenceFailureController();
|
||||
});
|
||||
|
||||
it("converts timestamps to human-readable dates", function () {
|
||||
expect(controller.formatTimestamp(402514331000))
|
||||
.toEqual("1982-10-03 17:32:11Z");
|
||||
});
|
||||
|
||||
it("provides default user names", function () {
|
||||
expect(controller.formatUsername(undefined))
|
||||
.toEqual(jasmine.any(String));
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,38 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceFailureDialog", "../src/PersistenceFailureConstants"],
|
||||
function (PersistenceFailureDialog, Constants) {
|
||||
"use strict";
|
||||
|
||||
describe("The persistence failure dialog", function () {
|
||||
var testFailures,
|
||||
dialog;
|
||||
|
||||
beforeEach(function () {
|
||||
testFailures = [
|
||||
{ error: { key: Constants.REVISION_ERROR_KEY }, someKey: "abc" },
|
||||
{ error: { key: "..." }, someKey: "def" },
|
||||
{ error: { key: Constants.REVISION_ERROR_KEY }, someKey: "ghi" },
|
||||
{ error: { key: Constants.REVISION_ERROR_KEY }, someKey: "jkl" },
|
||||
{ error: { key: "..." }, someKey: "mno" }
|
||||
];
|
||||
dialog = new PersistenceFailureDialog(testFailures);
|
||||
});
|
||||
|
||||
it("categorizes failures", function () {
|
||||
expect(dialog.model.revised).toEqual([
|
||||
testFailures[0], testFailures[2], testFailures[3]
|
||||
]);
|
||||
expect(dialog.model.unrecoverable).toEqual([
|
||||
testFailures[1], testFailures[4]
|
||||
]);
|
||||
});
|
||||
|
||||
it("provides an overwrite option", function () {
|
||||
expect(dialog.options[0].key).toEqual(Constants.OVERWRITE_KEY);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,97 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceFailureHandler", "../src/PersistenceFailureConstants"],
|
||||
function (PersistenceFailureHandler, Constants) {
|
||||
"use strict";
|
||||
|
||||
describe("The persistence failure handler", function () {
|
||||
var mockQ,
|
||||
mockDialogService,
|
||||
mockFailures,
|
||||
mockPromise,
|
||||
handler;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeMockFailure(id, index) {
|
||||
var mockFailure = jasmine.createSpyObj(
|
||||
'failure-' + id,
|
||||
['requeue']
|
||||
),
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
'persistence-' + id,
|
||||
['refresh', 'persist']
|
||||
);
|
||||
mockFailure.domainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability', 'useCapability', 'getModel']
|
||||
);
|
||||
mockFailure.domainObject.getCapability.andCallFake(function (c) {
|
||||
return (c === 'persistence') && mockPersistence;
|
||||
});
|
||||
mockFailure.domainObject.getModel.andReturn({ id: id, modified: index });
|
||||
mockFailure.persistence = mockPersistence;
|
||||
mockFailure.id = id;
|
||||
mockFailure.error = { key: Constants.REVISION_ERROR_KEY };
|
||||
return mockFailure;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj('$q', ['all', 'when']);
|
||||
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserChoice']);
|
||||
mockFailures = ['a', 'b', 'c'].map(makeMockFailure);
|
||||
mockPromise = jasmine.createSpyObj('promise', ['then']);
|
||||
mockDialogService.getUserChoice.andReturn(mockPromise);
|
||||
mockQ.all.andReturn(mockPromise);
|
||||
mockPromise.then.andReturn(mockPromise);
|
||||
handler = new PersistenceFailureHandler(mockQ, mockDialogService);
|
||||
});
|
||||
|
||||
it("shows a dialog to handle failures", function () {
|
||||
handler.handle(mockFailures);
|
||||
expect(mockDialogService.getUserChoice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("overwrites on request", function () {
|
||||
mockQ.all.andReturn(asPromise([]));
|
||||
handler.handle(mockFailures);
|
||||
// User chooses overwrite
|
||||
mockPromise.then.mostRecentCall.args[0](Constants.OVERWRITE_KEY);
|
||||
// Should refresh, remutate, and requeue all objects
|
||||
mockFailures.forEach(function (mockFailure, i) {
|
||||
expect(mockFailure.persistence.refresh).toHaveBeenCalled();
|
||||
expect(mockFailure.requeue).toHaveBeenCalled();
|
||||
expect(mockFailure.domainObject.useCapability).toHaveBeenCalledWith(
|
||||
'mutation',
|
||||
jasmine.any(Function),
|
||||
i // timestamp
|
||||
);
|
||||
expect(mockFailure.domainObject.useCapability.mostRecentCall.args[1]())
|
||||
.toEqual({ id: mockFailure.id, modified: i });
|
||||
});
|
||||
});
|
||||
|
||||
it("discards on request", function () {
|
||||
mockQ.all.andReturn(asPromise([]));
|
||||
handler.handle(mockFailures);
|
||||
// User chooses overwrite
|
||||
mockPromise.then.mostRecentCall.args[0](false);
|
||||
// Should refresh, but not remutate, and requeue all objects
|
||||
mockFailures.forEach(function (mockFailure, i) {
|
||||
expect(mockFailure.persistence.refresh).toHaveBeenCalled();
|
||||
expect(mockFailure.requeue).not.toHaveBeenCalled();
|
||||
expect(mockFailure.domainObject.useCapability).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
116
platform/persistence/queue/test/PersistenceQueueHandlerSpec.js
Normal file
116
platform/persistence/queue/test/PersistenceQueueHandlerSpec.js
Normal file
@ -0,0 +1,116 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceQueueHandler"],
|
||||
function (PersistenceQueueHandler) {
|
||||
"use strict";
|
||||
|
||||
var TEST_ERROR = { someKey: "some value" };
|
||||
|
||||
describe("The persistence queue handler", function () {
|
||||
var mockQ,
|
||||
mockFailureHandler,
|
||||
mockPersistences,
|
||||
mockDomainObjects,
|
||||
mockQueue,
|
||||
mockRejection,
|
||||
handler;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeMockPersistence(id) {
|
||||
var mockPersistence = jasmine.createSpyObj(
|
||||
'persistence-' + id,
|
||||
[ 'persist', 'refresh' ]
|
||||
);
|
||||
mockPersistence.persist.andReturn(asPromise(true));
|
||||
return mockPersistence;
|
||||
}
|
||||
|
||||
function makeMockDomainObject(id) {
|
||||
var mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject-' + id,
|
||||
[ 'getId' ]
|
||||
);
|
||||
mockDomainObject.getId.andReturn(id);
|
||||
return mockDomainObject;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj('$q', ['all']);
|
||||
mockFailureHandler = jasmine.createSpyObj('handler', ['handle']);
|
||||
mockQueue = jasmine.createSpyObj('queue', ['put']);
|
||||
mockPersistences = {};
|
||||
mockDomainObjects = {};
|
||||
['a', 'b', 'c'].forEach(function (id) {
|
||||
mockPersistences[id] = makeMockPersistence(id);
|
||||
mockDomainObjects[id] = makeMockDomainObject(id);
|
||||
});
|
||||
mockRejection = jasmine.createSpyObj('rejection', ['then']);
|
||||
mockQ.all.andReturn(asPromise([]));
|
||||
mockRejection.then.andCallFake(function (callback, fallback) {
|
||||
return asPromise(fallback({ someKey: "some value" }));
|
||||
});
|
||||
handler = new PersistenceQueueHandler(mockQ, mockFailureHandler);
|
||||
});
|
||||
|
||||
it("invokes persistence on all members in the group", function () {
|
||||
handler.persist(mockPersistences, mockDomainObjects, mockQueue);
|
||||
expect(mockPersistences.a.persist).toHaveBeenCalled();
|
||||
expect(mockPersistences.b.persist).toHaveBeenCalled();
|
||||
expect(mockPersistences.c.persist).toHaveBeenCalled();
|
||||
// No failures in this group
|
||||
expect(mockFailureHandler.handle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles failures that occur", function () {
|
||||
mockPersistences.b.persist.andReturn(mockRejection);
|
||||
mockPersistences.c.persist.andReturn(mockRejection);
|
||||
handler.persist(mockPersistences, mockDomainObjects, mockQueue);
|
||||
expect(mockFailureHandler.handle).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 'b',
|
||||
persistence: mockPersistences.b,
|
||||
domainObject: mockDomainObjects.b,
|
||||
requeue: jasmine.any(Function),
|
||||
error: TEST_ERROR
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
persistence: mockPersistences.c,
|
||||
domainObject: mockDomainObjects.c,
|
||||
requeue: jasmine.any(Function),
|
||||
error: TEST_ERROR
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("provides a requeue method for failures", function () {
|
||||
// This method is needed by PersistenceFailureHandler
|
||||
// to allow requeuing of objects for persistence when
|
||||
// Overwrite is chosen.
|
||||
mockPersistences.b.persist.andReturn(mockRejection);
|
||||
handler.persist(mockPersistences, mockDomainObjects, mockQueue);
|
||||
|
||||
// Verify precondition
|
||||
expect(mockQueue.put).not.toHaveBeenCalled();
|
||||
|
||||
// Invoke requeue
|
||||
mockFailureHandler.handle.mostRecentCall.args[0][0].requeue();
|
||||
|
||||
// Should have returned the object to the queue
|
||||
expect(mockQueue.put).toHaveBeenCalledWith(
|
||||
mockDomainObjects.b,
|
||||
mockPersistences.b
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
133
platform/persistence/queue/test/PersistenceQueueImplSpec.js
Normal file
133
platform/persistence/queue/test/PersistenceQueueImplSpec.js
Normal file
@ -0,0 +1,133 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceQueueImpl"],
|
||||
function (PersistenceQueueImpl) {
|
||||
"use strict";
|
||||
|
||||
var TEST_DELAY = 42;
|
||||
|
||||
describe("The implemented persistence queue", function () {
|
||||
var mockQ,
|
||||
mockTimeout,
|
||||
mockHandler,
|
||||
mockDeferred,
|
||||
mockPromise,
|
||||
queue;
|
||||
|
||||
function makeMockDomainObject(id) {
|
||||
var mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject-' + id,
|
||||
[ 'getId' ]
|
||||
);
|
||||
mockDomainObject.getId.andReturn(id);
|
||||
return mockDomainObject;
|
||||
}
|
||||
|
||||
function makeMockPersistence(id) {
|
||||
var mockPersistence = jasmine.createSpyObj(
|
||||
'persistence-' + id,
|
||||
[ 'persist' ]
|
||||
);
|
||||
return mockPersistence;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj('$q', ['when', 'defer']);
|
||||
mockTimeout = jasmine.createSpy('$timeout');
|
||||
mockHandler = jasmine.createSpyObj('handler', ['persist']);
|
||||
mockDeferred = jasmine.createSpyObj('deferred', ['resolve']);
|
||||
mockDeferred.promise = jasmine.createSpyObj('promise', ['then']);
|
||||
mockPromise = jasmine.createSpyObj('promise', ['then']);
|
||||
mockQ.defer.andReturn(mockDeferred);
|
||||
mockTimeout.andReturn({});
|
||||
mockHandler.persist.andReturn(mockPromise);
|
||||
mockPromise.then.andReturn(mockPromise);
|
||||
queue = new PersistenceQueueImpl(
|
||||
mockQ,
|
||||
mockTimeout,
|
||||
mockHandler,
|
||||
TEST_DELAY
|
||||
);
|
||||
});
|
||||
|
||||
it("schedules a timeout to persist objects", function () {
|
||||
expect(mockTimeout).not.toHaveBeenCalled();
|
||||
queue.put(makeMockDomainObject('a'), makeMockPersistence('a'));
|
||||
expect(mockTimeout).toHaveBeenCalledWith(
|
||||
jasmine.any(Function),
|
||||
TEST_DELAY,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("does not schedule multiple timeouts for multiple objects", function () {
|
||||
// Put three objects in without triggering the timeout;
|
||||
// shouldn't schedule multiple timeouts
|
||||
queue.put(makeMockDomainObject('a'), makeMockPersistence('a'));
|
||||
queue.put(makeMockDomainObject('b'), makeMockPersistence('b'));
|
||||
queue.put(makeMockDomainObject('c'), makeMockPersistence('c'));
|
||||
expect(mockTimeout.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("returns a promise", function () {
|
||||
expect(queue.put(makeMockDomainObject('a'), makeMockPersistence('a')))
|
||||
.toEqual(mockDeferred.promise);
|
||||
});
|
||||
|
||||
it("waits for quiescence to proceed", function () {
|
||||
// Keep adding objects to the queue between timeouts.
|
||||
// Should keep scheduling timeouts instead of resolving.
|
||||
queue.put(makeMockDomainObject('a'), makeMockPersistence('a'));
|
||||
expect(mockTimeout.calls.length).toEqual(1);
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
queue.put(makeMockDomainObject('b'), makeMockPersistence('b'));
|
||||
expect(mockTimeout.calls.length).toEqual(2);
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
queue.put(makeMockDomainObject('c'), makeMockPersistence('c'));
|
||||
expect(mockTimeout.calls.length).toEqual(3);
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
expect(mockHandler.persist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists upon quiescence", function () {
|
||||
// Add objects to the queue, but fire two timeouts afterward
|
||||
queue.put(makeMockDomainObject('a'), makeMockPersistence('a'));
|
||||
queue.put(makeMockDomainObject('b'), makeMockPersistence('b'));
|
||||
queue.put(makeMockDomainObject('c'), makeMockPersistence('c'));
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
expect(mockHandler.persist).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("waits on an active flush, while flushing", function () {
|
||||
// Persist some objects
|
||||
queue.put(makeMockDomainObject('a'), makeMockPersistence('a'));
|
||||
queue.put(makeMockDomainObject('b'), makeMockPersistence('b'));
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
expect(mockTimeout.calls.length).toEqual(2);
|
||||
// Adding a new object should not trigger a new timeout,
|
||||
// because we haven't completed the previous flush
|
||||
queue.put(makeMockDomainObject('c'), makeMockPersistence('c'));
|
||||
expect(mockTimeout.calls.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("clears the active flush after it has completed", function () {
|
||||
// Persist some objects
|
||||
queue.put(makeMockDomainObject('a'), makeMockPersistence('a'));
|
||||
queue.put(makeMockDomainObject('b'), makeMockPersistence('b'));
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
expect(mockTimeout.calls.length).toEqual(2);
|
||||
// Resolve the promise from handler.persist
|
||||
mockPromise.then.calls[0].args[0](true);
|
||||
// Adding a new object should now trigger a new timeout,
|
||||
// because we have completed the previous flush
|
||||
queue.put(makeMockDomainObject('c'), makeMockPersistence('c'));
|
||||
expect(mockTimeout.calls.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
35
platform/persistence/queue/test/PersistenceQueueSpec.js
Normal file
35
platform/persistence/queue/test/PersistenceQueueSpec.js
Normal file
@ -0,0 +1,35 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/PersistenceQueue"],
|
||||
function (PersistenceQueue) {
|
||||
"use strict";
|
||||
|
||||
describe("The persistence queue", function () {
|
||||
var mockQ,
|
||||
mockTimeout,
|
||||
mockDialogService,
|
||||
queue;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj("$q", ['defer']);
|
||||
mockTimeout = jasmine.createSpy("$timeout");
|
||||
mockDialogService = jasmine.createSpyObj(
|
||||
'dialogService',
|
||||
['getUserChoice']
|
||||
);
|
||||
queue = new PersistenceQueue(mockQ, mockTimeout, mockDialogService);
|
||||
});
|
||||
|
||||
// PersistenceQueue is just responsible for handling injected
|
||||
// dependencies and wiring the PersistenceQueueImpl and its
|
||||
// handlers. Functionality is tested there, so our test here is
|
||||
// minimal (get back expected interface, no exceptions)
|
||||
it("provides a queue with a put method", function () {
|
||||
expect(queue.put).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,63 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/QueuingPersistenceCapabilityDecorator"],
|
||||
function (QueuingPersistenceCapabilityDecorator) {
|
||||
"use strict";
|
||||
|
||||
describe("A queuing persistence capability decorator", function () {
|
||||
var mockQueue,
|
||||
mockCapabilityService,
|
||||
mockPersistenceConstructor,
|
||||
mockPersistence,
|
||||
mockDomainObject,
|
||||
testModel,
|
||||
decorator;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQueue = jasmine.createSpyObj('queue', ['put']);
|
||||
mockCapabilityService = jasmine.createSpyObj(
|
||||
'capabilityService',
|
||||
['getCapabilities']
|
||||
);
|
||||
testModel = { someKey: "some value" };
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
'persistence',
|
||||
['persist', 'refresh']
|
||||
);
|
||||
mockPersistenceConstructor = jasmine.createSpy();
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getId']
|
||||
);
|
||||
|
||||
mockCapabilityService.getCapabilities.andReturn({
|
||||
persistence: mockPersistenceConstructor
|
||||
});
|
||||
mockPersistenceConstructor.andReturn(mockPersistence);
|
||||
|
||||
decorator = new QueuingPersistenceCapabilityDecorator(
|
||||
mockQueue,
|
||||
mockCapabilityService
|
||||
);
|
||||
});
|
||||
|
||||
// Here, we verify that the decorator wraps the calls it is expected
|
||||
// to wrap; remaining responsibility belongs to
|
||||
// QueuingPersistenceCapability itself, which has its own tests.
|
||||
|
||||
it("delegates to its wrapped service", function () {
|
||||
decorator.getCapabilities(testModel);
|
||||
expect(mockCapabilityService.getCapabilities)
|
||||
.toHaveBeenCalledWith(testModel);
|
||||
});
|
||||
|
||||
it("wraps its persistence capability's constructor", function () {
|
||||
decorator.getCapabilities(testModel).persistence(mockDomainObject);
|
||||
expect(mockPersistenceConstructor).toHaveBeenCalledWith(mockDomainObject);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,46 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/QueuingPersistenceCapability"],
|
||||
function (QueuingPersistenceCapability) {
|
||||
"use strict";
|
||||
|
||||
describe("A queuing persistence capability", function () {
|
||||
var mockQueue,
|
||||
mockPersistence,
|
||||
mockDomainObject,
|
||||
persistence;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQueue = jasmine.createSpyObj('queue', ['put']);
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
'persistence',
|
||||
['persist', 'refresh']
|
||||
);
|
||||
mockDomainObject = {};
|
||||
persistence = new QueuingPersistenceCapability(
|
||||
mockQueue,
|
||||
mockPersistence,
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
||||
it("puts a request for persistence into the queue on persist", function () {
|
||||
// Verify precondition
|
||||
expect(mockQueue.put).not.toHaveBeenCalled();
|
||||
// Invoke persistence
|
||||
persistence.persist();
|
||||
// Should have queued
|
||||
expect(mockQueue.put).toHaveBeenCalledWith(
|
||||
mockDomainObject,
|
||||
mockPersistence
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes other methods from the wrapped persistence capability", function () {
|
||||
expect(persistence.refresh).toBe(mockPersistence.refresh);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
11
platform/persistence/queue/test/suite.json
Normal file
11
platform/persistence/queue/test/suite.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
"PersistenceFailureConstants",
|
||||
"PersistenceFailureController",
|
||||
"PersistenceFailureDialog",
|
||||
"PersistenceFailureHandler",
|
||||
"PersistenceQueue",
|
||||
"PersistenceQueueHandler",
|
||||
"PersistenceQueueImpl",
|
||||
"QueuingPersistenceCapability",
|
||||
"QueuingPersistenceCapabilityDecorator"
|
||||
]
|
Loading…
Reference in New Issue
Block a user