From 2d64813a4fa5bfb69f16f7ab8e2753f73f232a8b Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Fri, 17 Dec 2021 12:14:35 -0800 Subject: [PATCH] Rewrite local storage (#4583) * Reimplementation of Local Storage provider * Added tests * Remove identifierService mocks from all test specs * Do not persist identifiers in couch * Constant rename * Fix broken test * Clean up mock window functions * Updated copyright notice * Fixed bug in in-memory search indexer --- platform/persistence/local/README.md | 9 -- platform/persistence/local/bundle.js | 61 ---------- .../local/src/LocalStorageIndicator.js | 61 ---------- .../src/LocalStoragePersistenceProvider.js | 97 --------------- .../local/test/LocalStorageIndicatorSpec.js | 58 --------- .../LocalStoragePersistenceProviderSpec.js | 113 ------------------ src/api/actions/ActionCollectionSpec.js | 12 -- src/api/objects/InMemorySearchProvider.js | 2 +- src/api/objects/ObjectAPI.js | 21 +--- src/api/objects/ObjectAPISpec.js | 13 -- src/api/objects/TransactionSpec.js | 3 +- src/installDefaultBundles.js | 1 - src/plugins/condition/pluginSpec.js | 15 --- .../LocalStorageObjectProvider.js | 100 ++++++++++++++++ src/plugins/localStorage/plugin.js | 29 +++++ src/plugins/localStorage/pluginSpec.js | 96 +++++++++++++++ .../notebook/utils/notebook-entriesSpec.js | 13 -- .../notebook/utils/notebook-storageSpec.js | 12 -- .../persistence/couch/CouchObjectProvider.js | 45 +++++-- src/plugins/persistence/couch/plugin.js | 8 +- src/plugins/persistence/couch/pluginSpec.js | 11 +- src/plugins/plugins.js | 8 +- 22 files changed, 281 insertions(+), 507 deletions(-) delete mode 100644 platform/persistence/local/README.md delete mode 100644 platform/persistence/local/bundle.js delete mode 100644 platform/persistence/local/src/LocalStorageIndicator.js delete mode 100644 platform/persistence/local/src/LocalStoragePersistenceProvider.js delete mode 100644 platform/persistence/local/test/LocalStorageIndicatorSpec.js delete mode 100644 platform/persistence/local/test/LocalStoragePersistenceProviderSpec.js create mode 100644 src/plugins/localStorage/LocalStorageObjectProvider.js create mode 100644 src/plugins/localStorage/plugin.js create mode 100644 src/plugins/localStorage/pluginSpec.js diff --git a/platform/persistence/local/README.md b/platform/persistence/local/README.md deleted file mode 100644 index 553fa499a3..0000000000 --- a/platform/persistence/local/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Local Storage Plugin -Provides persistence of user-created objects in browser Local Storage. Objects persisted in this way will only be -available from the browser and machine on which they were persisted. For shared persistence, consider the -[Elasticsearch](../elastic/) and [CouchDB](../couch/) persistence plugins. - -## Installation -```js -openmct.install(openmct.plugins.LocalStorage()); -``` \ No newline at end of file diff --git a/platform/persistence/local/bundle.js b/platform/persistence/local/bundle.js deleted file mode 100644 index 9c15020193..0000000000 --- a/platform/persistence/local/bundle.js +++ /dev/null @@ -1,61 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define([ - "./src/LocalStoragePersistenceProvider", - "./src/LocalStorageIndicator" -], function ( - LocalStoragePersistenceProvider, - LocalStorageIndicator -) { - - return { - name: "platform/persistence/local", - definition: { - "extensions": { - "components": [ - { - "provides": "persistenceService", - "type": "provider", - "implementation": LocalStoragePersistenceProvider, - "depends": [ - "$window", - "$q", - "PERSISTENCE_SPACE" - ] - } - ], - "constants": [ - { - "key": "PERSISTENCE_SPACE", - "value": "mct" - } - ], - "indicators": [ - { - "implementation": LocalStorageIndicator - } - ] - } - } - }; -}); diff --git a/platform/persistence/local/src/LocalStorageIndicator.js b/platform/persistence/local/src/LocalStorageIndicator.js deleted file mode 100644 index 5500e67ca8..0000000000 --- a/platform/persistence/local/src/LocalStorageIndicator.js +++ /dev/null @@ -1,61 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [], - function () { - - var LOCAL_STORAGE_WARNING = [ - "Using browser local storage for persistence.", - "Anything you create or change will only be saved", - "in this browser on this machine." - ].join(' '); - - /** - * Indicator for local storage persistence. Provides a minimum - * level of feedback indicating that local storage is in use. - * @constructor - * @memberof platform/persistence/local - * @implements {Indicator} - */ - function LocalStorageIndicator() { - } - - LocalStorageIndicator.prototype.getCssClass = function () { - return "c-indicator--clickable icon-suitcase s-status-caution"; - }; - - LocalStorageIndicator.prototype.getGlyphClass = function () { - return 'caution'; - }; - - LocalStorageIndicator.prototype.getText = function () { - return "Off-line storage"; - }; - - LocalStorageIndicator.prototype.getDescription = function () { - return LOCAL_STORAGE_WARNING; - }; - - return LocalStorageIndicator; - } -); diff --git a/platform/persistence/local/src/LocalStoragePersistenceProvider.js b/platform/persistence/local/src/LocalStoragePersistenceProvider.js deleted file mode 100644 index 2e087a14d2..0000000000 --- a/platform/persistence/local/src/LocalStoragePersistenceProvider.js +++ /dev/null @@ -1,97 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [], - function () { - - /** - * The LocalStoragePersistenceProvider reads and writes JSON documents - * (more specifically, domain object models) to/from the browser's - * local storage. - * @memberof platform/persistence/local - * @constructor - * @implements {PersistenceService} - * @param q Angular's $q, for promises - * @param $interval Angular's $interval service - * @param {string} space the name of the persistence space being served - */ - function LocalStoragePersistenceProvider($window, $q, space) { - this.$q = $q; - this.space = space; - this.spaces = space ? [space] : []; - this.localStorage = $window.localStorage; - } - - /** - * Set a value in local storage. - * @private - */ - LocalStoragePersistenceProvider.prototype.setValue = function (key, value) { - this.localStorage[key] = JSON.stringify(value); - }; - - /** - * Get a value from local storage. - * @private - */ - LocalStoragePersistenceProvider.prototype.getValue = function (key) { - return this.localStorage[key] - ? JSON.parse(this.localStorage[key]) : {}; - }; - - LocalStoragePersistenceProvider.prototype.listSpaces = function () { - return this.$q.when(this.spaces); - }; - - LocalStoragePersistenceProvider.prototype.listObjects = function (space) { - return this.$q.when(Object.keys(this.getValue(space))); - }; - - LocalStoragePersistenceProvider.prototype.createObject = function (space, key, value) { - var spaceObj = this.getValue(space); - spaceObj[key] = value; - this.setValue(space, spaceObj); - - return this.$q.when(true); - }; - - LocalStoragePersistenceProvider.prototype.readObject = function (space, key) { - var spaceObj = this.getValue(space); - - return this.$q.when(spaceObj[key]); - }; - - LocalStoragePersistenceProvider.prototype.deleteObject = function (space, key) { - var spaceObj = this.getValue(space); - delete spaceObj[key]; - this.setValue(space, spaceObj); - - return this.$q.when(true); - }; - - LocalStoragePersistenceProvider.prototype.updateObject = - LocalStoragePersistenceProvider.prototype.createObject; - - return LocalStoragePersistenceProvider; - } -); diff --git a/platform/persistence/local/test/LocalStorageIndicatorSpec.js b/platform/persistence/local/test/LocalStorageIndicatorSpec.js deleted file mode 100644 index 52042d70ea..0000000000 --- a/platform/persistence/local/test/LocalStorageIndicatorSpec.js +++ /dev/null @@ -1,58 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - ["../src/LocalStorageIndicator"], - function (LocalStorageIndicator) { - - xdescribe("The local storage status indicator", function () { - var indicator; - - beforeEach(function () { - indicator = new LocalStorageIndicator(); - }); - - it("provides text to display in status area", function () { - // Don't particularly care what is there so long - // as interface is appropriately implemented. - expect(indicator.getText()).toEqual(jasmine.any(String)); - }); - - it("has a database icon", function () { - expect(indicator.getCssClass()).toEqual("icon-suitcase s-status-caution"); - }); - - it("has a 'caution' class to draw attention", function () { - expect(indicator.getGlyphClass()).toEqual("caution"); - }); - - it("provides a description for a tooltip", function () { - // Just want some non-empty string here. Providing a - // message here is important but don't want to test wording. - var description = indicator.getDescription(); - expect(description).toEqual(jasmine.any(String)); - expect(description.length).not.toEqual(0); - }); - - }); - } -); diff --git a/platform/persistence/local/test/LocalStoragePersistenceProviderSpec.js b/platform/persistence/local/test/LocalStoragePersistenceProviderSpec.js deleted file mode 100644 index e92b0f30d6..0000000000 --- a/platform/persistence/local/test/LocalStoragePersistenceProviderSpec.js +++ /dev/null @@ -1,113 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - ["../src/LocalStoragePersistenceProvider"], - function (LocalStoragePersistenceProvider) { - - describe("The local storage persistence provider", function () { - var mockQ, - testSpace = "testSpace", - mockCallback, - testLocalStorage, - provider; - - function mockPromise(value) { - return (value || {}).then ? value : { - then: function (callback) { - return mockPromise(callback(value)); - } - }; - } - - beforeEach(function () { - testLocalStorage = {}; - - mockQ = jasmine.createSpyObj("$q", ["when", "reject"]); - mockCallback = jasmine.createSpy('callback'); - - mockQ.when.and.callFake(mockPromise); - - provider = new LocalStoragePersistenceProvider( - { localStorage: testLocalStorage }, - mockQ, - testSpace - ); - }); - - it("reports available spaces", function () { - provider.listSpaces().then(mockCallback); - expect(mockCallback).toHaveBeenCalledWith([testSpace]); - }); - - it("lists all available documents", function () { - provider.listObjects(testSpace).then(mockCallback); - expect(mockCallback.calls.mostRecent().args[0]).toEqual([]); - provider.createObject(testSpace, 'abc', { a: 42 }); - provider.listObjects(testSpace).then(mockCallback); - expect(mockCallback.calls.mostRecent().args[0]).toEqual(['abc']); - }); - - it("allows object creation", function () { - var model = { someKey: "some value" }; - provider.createObject(testSpace, "abc", model) - .then(mockCallback); - expect(JSON.parse(testLocalStorage[testSpace]).abc) - .toEqual(model); - expect(mockCallback.calls.mostRecent().args[0]).toBeTruthy(); - }); - - it("allows object models to be read back", function () { - var model = { someKey: "some other value" }; - testLocalStorage[testSpace] = JSON.stringify({ abc: model }); - provider.readObject(testSpace, "abc").then(mockCallback); - expect(mockCallback).toHaveBeenCalledWith(model); - }); - - it("allows object update", function () { - var model = { someKey: "some new value" }; - testLocalStorage[testSpace] = JSON.stringify({ - abc: { somethingElse: 42 } - }); - provider.updateObject(testSpace, "abc", model) - .then(mockCallback); - expect(JSON.parse(testLocalStorage[testSpace]).abc) - .toEqual(model); - }); - - it("allows object deletion", function () { - testLocalStorage[testSpace] = JSON.stringify({ - abc: { somethingElse: 42 } - }); - provider.deleteObject(testSpace, "abc").then(mockCallback); - expect(testLocalStorage[testSpace].abc) - .toBeUndefined(); - }); - - it("returns undefined when objects are not found", function () { - provider.readObject("testSpace", "abc").then(mockCallback); - expect(mockCallback).toHaveBeenCalledWith(undefined); - }); - - }); - } -); diff --git a/src/api/actions/ActionCollectionSpec.js b/src/api/actions/ActionCollectionSpec.js index 6acbd2671a..99145b7673 100644 --- a/src/api/actions/ActionCollectionSpec.js +++ b/src/api/actions/ActionCollectionSpec.js @@ -29,22 +29,10 @@ describe('The ActionCollection', () => { let mockApplicableActions; let mockObjectPath; let mockView; - let mockIdentifierService; beforeEach(() => { openmct = createOpenMct(); - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - mockIdentifierService = jasmine.createSpyObj( - 'identifierService', - ['parse'] - ); - mockIdentifierService.parse.and.returnValue({ - getSpace: () => { - return ''; - } - }); - openmct.$injector.get.and.returnValue(mockIdentifierService); mockObjectPath = [ { name: 'mock folder', diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js index ae88a526c4..ca99ff1e4d 100644 --- a/src/api/objects/InMemorySearchProvider.js +++ b/src/api/objects/InMemorySearchProvider.js @@ -187,7 +187,7 @@ class InMemorySearchProvider { */ scheduleForIndexing(identifier) { const keyString = this.openmct.objects.makeKeyString(identifier); - const objectProvider = this.openmct.objects.getProvider(identifier.key); + const objectProvider = this.openmct.objects.getProvider(identifier); if (objectProvider === undefined || objectProvider.search === undefined) { if (!this.indexedIds[keyString] && !this.pendingIndex[keyString]) { diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index b2cfa949b2..56fcde50e7 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -43,9 +43,6 @@ function ObjectAPI(typeRegistry, openmct) { this.providers = {}; this.rootRegistry = new RootRegistry(); this.inMemorySearchProvider = new InMemorySearchProvider(openmct); - this.injectIdentifierService = function () { - this.identifierService = this.openmct.$injector.get("identifierService"); - }; this.rootProvider = new RootObjectProvider(this.rootRegistry); this.cache = {}; @@ -66,33 +63,17 @@ ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) { this.fallbackProvider = p; }; -/** - * @private - */ -ObjectAPI.prototype.getIdentifierService = function () { - // Lazily acquire identifier service - if (!this.identifierService) { - this.injectIdentifierService(); - } - - return this.identifierService; -}; - /** * Retrieve the provider for a given identifier. * @private */ ObjectAPI.prototype.getProvider = function (identifier) { - //handles the '' vs 'mct' namespace issue - const keyString = utils.makeKeyString(identifier); - const identifierService = this.getIdentifierService(); - const namespace = identifierService.parse(keyString).getSpace(); if (identifier.key === 'ROOT') { return this.rootProvider; } - return this.providers[namespace] || this.fallbackProvider; + return this.providers[identifier.namespace] || this.fallbackProvider; }; /** diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index 10c6221c7c..8887a04c74 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -5,7 +5,6 @@ describe("The Object API", () => { let objectAPI; let typeRegistry; let openmct = {}; - let mockIdentifierService; let mockDomainObject; const TEST_NAMESPACE = "test-namespace"; const FIFTEEN_MINUTES = 15 * 60 * 1000; @@ -15,18 +14,6 @@ describe("The Object API", () => { 'get' ]); openmct = createOpenMct(); - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - mockIdentifierService = jasmine.createSpyObj( - 'identifierService', - ['parse'] - ); - mockIdentifierService.parse.and.returnValue({ - getSpace: () => { - return TEST_NAMESPACE; - } - }); - - openmct.$injector.get.and.returnValue(mockIdentifierService); objectAPI = openmct.objects; openmct.editor = {}; diff --git a/src/api/objects/TransactionSpec.js b/src/api/objects/TransactionSpec.js index e616b623f7..286401ad18 100644 --- a/src/api/objects/TransactionSpec.js +++ b/src/api/objects/TransactionSpec.js @@ -9,7 +9,7 @@ describe("Transaction Class", () => { beforeEach(() => { objectAPI = { makeKeyString: (identifier) => utils.makeKeyString(identifier), - save: (object) => Promise.resolve(object), + save: () => Promise.resolve(true), mutate: (object, prop, value) => { object[prop] = value; @@ -61,6 +61,7 @@ describe("Transaction Class", () => { expect(transaction.dirtyObjects.size).toEqual(3); spyOn(objectAPI, 'save').and.callThrough(); + transaction.commit() .then(success => { expect(transaction.dirtyObjects.size).toEqual(0); diff --git a/src/installDefaultBundles.js b/src/installDefaultBundles.js index 681d5bbdcd..e11e654f47 100644 --- a/src/installDefaultBundles.js +++ b/src/installDefaultBundles.js @@ -74,7 +74,6 @@ define([ '../platform/identity/bundle', '../platform/persistence/aggregator/bundle', '../platform/persistence/elastic/bundle', - '../platform/persistence/local/bundle', '../platform/persistence/queue/bundle', '../platform/policy/bundle', '../platform/representation/bundle', diff --git a/src/plugins/condition/pluginSpec.js b/src/plugins/condition/pluginSpec.js index c5e64833f4..e148872ce2 100644 --- a/src/plugins/condition/pluginSpec.js +++ b/src/plugins/condition/pluginSpec.js @@ -810,21 +810,6 @@ describe('the plugin', function () { openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); openmct.telemetry.request.and.returnValue(Promise.resolve([])); - // mockTransactionService.commit = async () => {}; - const mockIdentifierService = jasmine.createSpyObj( - 'identifierService', - ['parse'] - ); - mockIdentifierService.parse.and.returnValue({ - getSpace: () => { - return ''; - } - }); - - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService); - // .withArgs('transactionService').and.returnValue(mockTransactionService); - const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); spyOn(styleRuleManger, 'subscribeToConditionSet'); openmct.editor.edit(); diff --git a/src/plugins/localStorage/LocalStorageObjectProvider.js b/src/plugins/localStorage/LocalStorageObjectProvider.js new file mode 100644 index 0000000000..bf311c1a1c --- /dev/null +++ b/src/plugins/localStorage/LocalStorageObjectProvider.js @@ -0,0 +1,100 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +export default class LocalStorageObjectProvider { + constructor(spaceKey = 'mct') { + this.localStorage = window.localStorage; + this.spaceKey = spaceKey; + this.initializeSpace(spaceKey); + } + + get(identifier) { + if (this.getSpaceAsObject()[identifier.key] !== undefined) { + const persistedModel = this.getSpaceAsObject()[identifier.key]; + const domainObject = { + identifier, + ...persistedModel + }; + + return Promise.resolve(domainObject); + } else { + return Promise.resolve(undefined); + } + } + + create(object) { + return this.persistObject(object); + } + + update(object) { + return this.persistObject(object); + } + + /** + * @private + */ + persistObject(domainObject) { + let space = this.getSpaceAsObject(); + space[domainObject.identifier.key] = domainObject; + + this.persistSpace(space); + + return Promise.resolve(true); + } + + /** + * @private + */ + persistSpace(space) { + this.localStorage[this.spaceKey] = JSON.stringify(space); + } + + /** + * @private + */ + getSpace() { + return this.localStorage[this.spaceKey]; + } + + /** + * @private + */ + getSpaceAsObject() { + return JSON.parse(this.getSpace()); + } + + /** + * @private + */ + initializeSpace() { + if (this.isEmpty()) { + this.localStorage[this.spaceKey] = JSON.stringify({}); + } + } + + /** + * @private + */ + isEmpty() { + return this.getSpace() === undefined; + } +} diff --git a/src/plugins/localStorage/plugin.js b/src/plugins/localStorage/plugin.js new file mode 100644 index 0000000000..c9ccef642b --- /dev/null +++ b/src/plugins/localStorage/plugin.js @@ -0,0 +1,29 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import LocalStorageObjectProvider from './LocalStorageObjectProvider'; + +export default function (namespace = '', storageSpace = 'mct') { + return function (openmct) { + openmct.objects.addProvider(namespace, new LocalStorageObjectProvider(storageSpace)); + }; +} diff --git a/src/plugins/localStorage/pluginSpec.js b/src/plugins/localStorage/pluginSpec.js new file mode 100644 index 0000000000..e4c68d911d --- /dev/null +++ b/src/plugins/localStorage/pluginSpec.js @@ -0,0 +1,96 @@ +/* eslint-disable no-invalid-this */ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import { + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +describe("The local storage plugin", () => { + let space; + let openmct; + + beforeEach(() => { + space = `test-${Date.now()}`; + openmct = createOpenMct(); + + openmct.install(openmct.plugins.LocalStorage('', space)); + + }); + + it('initializes localstorage if not already initialized', () => { + const ls = getLocalStorage(); + expect(ls[space]).toBeDefined(); + }); + + it('successfully persists an object to localstorage', async () => { + const domainObject = { + identifier: { + namespace: '', + key: 'test-key' + }, + name: 'A test object' + }; + let spaceAsObject = getSpaceAsObject(); + expect(spaceAsObject['test-key']).not.toBeDefined(); + + await openmct.objects.save(domainObject); + + spaceAsObject = getSpaceAsObject(); + expect(spaceAsObject['test-key']).toBeDefined(); + }); + + it('successfully retrieves an object from localstorage', async () => { + const domainObject = { + identifier: { + namespace: '', + key: 'test-key' + }, + name: 'A test object', + anotherProperty: Date.now() + }; + await openmct.objects.save(domainObject); + + let testObject = await openmct.objects.get(domainObject.identifier); + + expect(testObject.name).toEqual(domainObject.name); + expect(testObject.anotherProperty).toEqual(domainObject.anotherProperty); + }); + + afterEach(() => { + resetApplicationState(openmct); + resetLocalStorage(); + }); + + function resetLocalStorage() { + delete window.localStorage[space]; + } + + function getLocalStorage() { + return window.localStorage; + } + + function getSpaceAsObject() { + return JSON.parse(getLocalStorage()[space]); + } +}); diff --git a/src/plugins/notebook/utils/notebook-entriesSpec.js b/src/plugins/notebook/utils/notebook-entriesSpec.js index 9fbcab5546..3bac23fb6d 100644 --- a/src/plugins/notebook/utils/notebook-entriesSpec.js +++ b/src/plugins/notebook/utils/notebook-entriesSpec.js @@ -95,23 +95,10 @@ const selectedPage = { }; let openmct; -let mockIdentifierService; describe('Notebook Entries:', () => { beforeEach(() => { openmct = createOpenMct(); - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - mockIdentifierService = jasmine.createSpyObj( - 'identifierService', - ['parse'] - ); - mockIdentifierService.parse.and.returnValue({ - getSpace: () => { - return ''; - } - }); - - openmct.$injector.get.and.returnValue(mockIdentifierService); openmct.types.addType('notebook', { creatable: true }); diff --git a/src/plugins/notebook/utils/notebook-storageSpec.js b/src/plugins/notebook/utils/notebook-storageSpec.js index ddad2b8152..77659a9f5a 100644 --- a/src/plugins/notebook/utils/notebook-storageSpec.js +++ b/src/plugins/notebook/utils/notebook-storageSpec.js @@ -64,23 +64,11 @@ const notebookStorage = { }; let openmct; -let mockIdentifierService; describe('Notebook Storage:', () => { beforeEach(() => { openmct = createOpenMct(); - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - mockIdentifierService = jasmine.createSpyObj( - 'identifierService', - ['parse'] - ); - mockIdentifierService.parse.and.returnValue({ - getSpace: () => { - return ''; - } - }); - openmct.$injector.get.and.returnValue(mockIdentifierService); window.localStorage.setItem('notebook-storage', null); openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [ 'create', diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 64ce7f5b37..e9a7dc6444 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -84,17 +84,17 @@ class CouchObjectProvider { this.changesFeedSharedWorkerConnectionId = event.data.connectionId; } else { let objectChanges = event.data.objectChanges; - objectChanges.identifier = { + const objectIdentifier = { namespace: this.namespace, key: objectChanges.id }; - let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier); + let keyString = this.openmct.objects.makeKeyString(objectIdentifier); //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. let observersForObject = this.observers[keyString]; if (observersForObject) { observersForObject.forEach(async (observer) => { - const updatedObject = await this.get(objectChanges.identifier); + const updatedObject = await this.get(objectIdentifier); if (this.isSynchronizedObject(updatedObject)) { observer(updatedObject); } @@ -179,11 +179,8 @@ class CouchObjectProvider { getModel(response) { if (response && response.model) { let key = response[ID]; - let object = response.model; - object.identifier = { - namespace: this.namespace, - key: key - }; + let object = this.fromPersistedModel(response.model, key); + if (!this.objectQueue[key]) { this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); } @@ -445,17 +442,17 @@ class CouchObjectProvider { } onEventMessage(event) { - const object = JSON.parse(event.data); - object.identifier = { + const eventData = JSON.parse(event.data); + const identifier = { namespace: this.namespace, - key: object.id + key: eventData.id }; - let keyString = this.openmct.objects.makeKeyString(object.identifier); + const keyString = this.openmct.objects.makeKeyString(identifier); let observersForObject = this.observers[keyString]; if (observersForObject) { observersForObject.forEach(async (observer) => { - const updatedObject = await this.get(object.identifier); + const updatedObject = await this.get(identifier); if (this.isSynchronizedObject(updatedObject)) { observer(updatedObject); } @@ -520,7 +517,9 @@ class CouchObjectProvider { create(model) { let intermediateResponse = this.getIntermediateResponse(); const key = model.identifier.key; + model = this.toPersistableModel(model); this.enqueueObject(key, model, intermediateResponse); + if (!this.objectQueue[key].pending) { this.objectQueue[key].pending = true; const queued = this.objectQueue[key].dequeue(); @@ -557,11 +556,31 @@ class CouchObjectProvider { update(model) { let intermediateResponse = this.getIntermediateResponse(); const key = model.identifier.key; + model = this.toPersistableModel(model); + this.enqueueObject(key, model, intermediateResponse); this.updateQueued(key); return intermediateResponse.promise; } + + toPersistableModel(model) { + //First make a copy so we are not mutating the provided model. + const persistableModel = JSON.parse(JSON.stringify(model)); + //Delete the identifier. Couch manages namespaces dynamically. + delete persistableModel.identifier; + + return persistableModel; + } + + fromPersistedModel(model, key) { + model.identifier = { + namespace: this.namespace, + key + }; + + return model; + } } CouchObjectProvider.HTTP_CONFLICT = 409; diff --git a/src/plugins/persistence/couch/plugin.js b/src/plugins/persistence/couch/plugin.js index 85353976f9..f6e9670969 100644 --- a/src/plugins/persistence/couch/plugin.js +++ b/src/plugins/persistence/couch/plugin.js @@ -22,11 +22,15 @@ import CouchObjectProvider from './CouchObjectProvider'; const NAMESPACE = ''; -const PERSISTENCE_SPACE = 'mct'; +const LEGACY_SPACE = 'mct'; export default function CouchPlugin(options) { return function install(openmct) { install.couchProvider = new CouchObjectProvider(openmct, options, NAMESPACE); - openmct.objects.addProvider(PERSISTENCE_SPACE, install.couchProvider); + + // Unfortunately, for historical reasons, Couch DB produces objects with a mix of namepaces (alternately "mct", and "") + // Installing the same provider under both namespaces means that it can respond to object gets for both namespaces. + openmct.objects.addProvider(LEGACY_SPACE, install.couchProvider); + openmct.objects.addProvider(NAMESPACE, install.couchProvider); }; } diff --git a/src/plugins/persistence/couch/pluginSpec.js b/src/plugins/persistence/couch/pluginSpec.js index 4d11512df1..4c1647044b 100644 --- a/src/plugins/persistence/couch/pluginSpec.js +++ b/src/plugins/persistence/couch/pluginSpec.js @@ -129,8 +129,9 @@ describe('the plugin', () => { it('works without Shared Workers', async () => { let sharedWorkerCallback; - const restoreSharedWorker = window.SharedWorker; + const cachedSharedWorker = window.SharedWorker; window.SharedWorker = undefined; + const mockEventSource = { addEventListener: (topic, addedListener) => { sharedWorkerCallback = addedListener; @@ -139,6 +140,8 @@ describe('the plugin', () => { sharedWorkerCallback = null; } }; + const cachedEventSource = window.EventSource; + window.EventSource = function (url) { return mockEventSource; }; @@ -163,17 +166,21 @@ describe('the plugin', () => { expect(result).toBeTrue(); expect(provider.create).toHaveBeenCalled(); expect(provider.startSharedWorker).not.toHaveBeenCalled(); + //Set modified timestamp it detects a change and persists the updated model. mockDomainObject.modified = mockDomainObject.persisted + 1; const updatedResult = await openmct.objects.save(mockDomainObject); openmct.objects.observe(mockDomainObject, '*', (updatedObject) => { }); + expect(updatedResult).toBeTrue(); expect(provider.update).toHaveBeenCalled(); expect(provider.fetchChanges).toHaveBeenCalled(); sharedWorkerCallback(fakeUpdateEvent); expect(provider.onEventMessage).toHaveBeenCalled(); - window.SharedWorker = restoreSharedWorker; + + window.SharedWorker = cachedSharedWorker; + window.EventSource = cachedEventSource; }); }); describe('batches requests', () => { diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 28c798babd..a146aefcc1 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -73,7 +73,8 @@ define([ './hyperlink/plugin', './clock/plugin', './DeviceClassifier/plugin', - './timer/plugin' + './timer/plugin', + './localStorage/plugin' ], function ( _, UTCTimeSystem, @@ -127,10 +128,10 @@ define([ Hyperlink, Clock, DeviceClassifier, - Timer + Timer, + LocalStorage ) { const bundleMap = { - LocalStorage: 'platform/persistence/local', Elasticsearch: 'platform/persistence/elastic' }; @@ -235,6 +236,7 @@ define([ plugins.Clock = Clock.default; plugins.Timer = Timer.default; plugins.DeviceClassifier = DeviceClassifier.default; + plugins.LocalStorage = LocalStorage.default; return plugins; });