From 9da750c3bbed15e9be8b723241026a587d6a4fe5 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Mon, 30 Nov 2020 10:50:24 -0800 Subject: [PATCH] Add object interceptor API to allow missing model and missing my-items handling (#3522) * Extends Object API to allow adding interceptors Co-authored-by: Andrew Henry --- src/MCT.js | 1 + src/api/objects/InterceptorRegistry.js | 66 +++++++++++++++++++ src/api/objects/InterceptorRegistrySpec.js | 0 src/api/objects/ObjectAPI.js | 28 ++++++++ src/api/objects/ObjectAPISpec.js | 48 ++++++++++++++ .../interceptors/missingObjectInterceptor.js | 40 +++++++++++ .../interceptors/myItemsInterceptor.js | 43 ++++++++++++ src/plugins/interceptors/plugin.js | 9 +++ .../persistence/couch/CouchObjectProvider.js | 3 +- src/plugins/plugins.js | 7 +- 10 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 src/api/objects/InterceptorRegistry.js create mode 100644 src/api/objects/InterceptorRegistrySpec.js create mode 100644 src/plugins/interceptors/missingObjectInterceptor.js create mode 100644 src/plugins/interceptors/myItemsInterceptor.js create mode 100644 src/plugins/interceptors/plugin.js diff --git a/src/MCT.js b/src/MCT.js index a5d6ce88b1..8e8f8ee215 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -282,6 +282,7 @@ define([ this.install(this.plugins.NotificationIndicator()); this.install(this.plugins.NewFolderAction()); this.install(this.plugins.ViewDatumAction()); + this.install(this.plugins.ObjectInterceptors()); } MCT.prototype = Object.create(EventEmitter.prototype); diff --git a/src/api/objects/InterceptorRegistry.js b/src/api/objects/InterceptorRegistry.js new file mode 100644 index 0000000000..f8a44326d0 --- /dev/null +++ b/src/api/objects/InterceptorRegistry.js @@ -0,0 +1,66 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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 InterceptorRegistry { + /** + * A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects. + * @interface InterceptorRegistry + * @memberof module:openmct + */ + constructor() { + this.interceptors = []; + } + + /** + * @interface InterceptorDef + * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object + * @property {function} invoke function that transforms the provided domain object and returns the transformed domain object + * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number + * @memberof module:openmct InterceptorRegistry# + */ + + /** + * Register a new object interceptor. + * + * @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add + * @method addInterceptor + * @memberof module:openmct.InterceptorRegistry# + */ + addInterceptor(interceptorDef) { + //TODO: sort by priority + this.interceptors.push(interceptorDef); + } + + /** + * Retrieve all interceptors applicable to a domain object. + * @method getInterceptors + * @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object + * @memberof module:openmct.InterceptorRegistry# + */ + getInterceptors(identifier, object) { + return this.interceptors.filter(interceptor => { + return typeof interceptor.appliesTo === 'function' + && interceptor.appliesTo(identifier, object); + }); + } + +} + diff --git a/src/api/objects/InterceptorRegistrySpec.js b/src/api/objects/InterceptorRegistrySpec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index 7fb21d89a5..c2747be611 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -26,6 +26,7 @@ define([ './MutableObject', './RootRegistry', './RootObjectProvider', + './InterceptorRegistry', 'EventEmitter' ], function ( _, @@ -33,6 +34,7 @@ define([ MutableObject, RootRegistry, RootObjectProvider, + InterceptorRegistry, EventEmitter ) { @@ -48,6 +50,7 @@ define([ this.rootRegistry = new RootRegistry(); this.rootProvider = new RootObjectProvider.default(this.rootRegistry); this.cache = {}; + this.interceptorRegistry = new InterceptorRegistry.default(); } /** @@ -177,6 +180,10 @@ define([ return objectPromise.then(result => { delete this.cache[keystring]; + const interceptors = this.listGetInterceptors(identifier, result); + interceptors.forEach(interceptor => { + result = interceptor.invoke(identifier, result); + }); return result; }); @@ -312,6 +319,27 @@ define([ }); }; + /** + * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get + * The domain object will be transformed after it is retrieved from the persistence store + * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef + * + * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add + * @method addGetInterceptor + * @memberof module:openmct.InterceptorRegistry# + */ + ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) { + this.interceptorRegistry.addInterceptor(interceptorDef); + }; + + /** + * Retrieve the interceptors for a given domain object. + * @private + */ + ObjectAPI.prototype.listGetInterceptors = function (identifier, object) { + return this.interceptorRegistry.getInterceptors(identifier, object); + }; + /** * Uniquely identifies a domain object. * diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index 22c037a50a..ad6293b729 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -63,12 +63,51 @@ describe("The Object API", () => { describe("The get function", () => { describe("when a provider is available", () => { let mockProvider; + let mockInterceptor; + let anotherMockInterceptor; + let notApplicableMockInterceptor; beforeEach(() => { mockProvider = jasmine.createSpyObj("mock provider", [ "get" ]); mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject)); + + mockInterceptor = jasmine.createSpyObj("mock interceptor", [ + "appliesTo", + "invoke" + ]); + mockInterceptor.appliesTo.and.returnValue(true); + mockInterceptor.invoke.and.callFake((identifier, object) => { + return Object.assign({ + changed: true + }, object); + }); + + anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [ + "appliesTo", + "invoke" + ]); + anotherMockInterceptor.appliesTo.and.returnValue(true); + anotherMockInterceptor.invoke.and.callFake((identifier, object) => { + return Object.assign({ + alsoChanged: true + }, object); + }); + + notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [ + "appliesTo", + "invoke" + ]); + notApplicableMockInterceptor.appliesTo.and.returnValue(false); + notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => { + return Object.assign({ + shouldNotBeChanged: true + }, object); + }); objectAPI.addProvider(TEST_NAMESPACE, mockProvider); + objectAPI.addGetInterceptor(mockInterceptor); + objectAPI.addGetInterceptor(anotherMockInterceptor); + objectAPI.addGetInterceptor(notApplicableMockInterceptor); }); it("Caches multiple requests for the same object", () => { @@ -78,6 +117,15 @@ describe("The Object API", () => { objectAPI.get(mockDomainObject.identifier); expect(mockProvider.get.calls.count()).toBe(1); }); + + it("applies any applicable interceptors", () => { + expect(mockDomainObject.changed).toBeUndefined(); + objectAPI.get(mockDomainObject.identifier).then((object) => { + expect(object.changed).toBeTrue(); + expect(object.alsoChanged).toBeTrue(); + expect(object.shouldNotBeChanged).toBeUndefined(); + }); + }); }); }); }); diff --git a/src/plugins/interceptors/missingObjectInterceptor.js b/src/plugins/interceptors/missingObjectInterceptor.js new file mode 100644 index 0000000000..029b65bf4e --- /dev/null +++ b/src/plugins/interceptors/missingObjectInterceptor.js @@ -0,0 +1,40 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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 function MissingObjectInterceptor(openmct) { + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return identifier.key !== 'mine'; + }, + invoke: (identifier, object) => { + if (object === undefined) { + return { + identifier, + type: 'unknown', + name: 'Missing: ' + openmct.objects.makeKeyString(identifier) + }; + } + + return object; + } + }); +} diff --git a/src/plugins/interceptors/myItemsInterceptor.js b/src/plugins/interceptors/myItemsInterceptor.js new file mode 100644 index 0000000000..ef92839e5b --- /dev/null +++ b/src/plugins/interceptors/myItemsInterceptor.js @@ -0,0 +1,43 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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 function MyItemsInterceptor(openmct) { + + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return identifier.key === 'mine'; + }, + invoke: (identifier, object) => { + if (object === undefined) { + return { + identifier, + "name": "My Items", + "type": "folder", + "composition": [], + "location": "ROOT" + }; + } + + return object; + } + }); +} diff --git a/src/plugins/interceptors/plugin.js b/src/plugins/interceptors/plugin.js new file mode 100644 index 0000000000..4460100941 --- /dev/null +++ b/src/plugins/interceptors/plugin.js @@ -0,0 +1,9 @@ +import missingObjectInterceptor from "./missingObjectInterceptor"; +import myItemsInterceptor from "./myItemsInterceptor"; + +export default function plugin() { + return function install(openmct) { + myItemsInterceptor(openmct); + missingObjectInterceptor(openmct); + }; +} diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 1df673eb7b..8c7c4d3bfb 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -87,7 +87,8 @@ export default class CouchObjectProvider { } //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress - if (!this.objectQueue[key].pending) { + //Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates. + if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) { this.objectQueue[key].updateRevision(response[REV]); } diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 03be9ec6c6..875918f8d3 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -59,7 +59,8 @@ define([ './persistence/couch/plugin', './defaultRootName/plugin', './timeline/plugin', - './viewDatumAction/plugin' + './viewDatumAction/plugin', + './interceptors/plugin' ], function ( _, UTCTimeSystem, @@ -99,7 +100,8 @@ define([ CouchDBPlugin, DefaultRootName, Timeline, - ViewDatumAction + ViewDatumAction, + ObjectInterceptors ) { const bundleMap = { LocalStorage: 'platform/persistence/local', @@ -194,6 +196,7 @@ define([ plugins.DefaultRootName = DefaultRootName.default; plugins.Timeline = Timeline.default; plugins.ViewDatumAction = ViewDatumAction.default; + plugins.ObjectInterceptors = ObjectInterceptors.default; return plugins; });