mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +00:00
Add support for multiple CouchDB databases, multiple namespaces, and readOnly
configurations (#7413)
* two namespaces appear * works with two databases * try to batch requests * fix indicators * add option to omit root for myitems * ready for review * ready for review * ready for review * typo in README * spelling * spelling * update readme * fix tests * Update src/plugins/persistence/couch/README.md Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * remove omitRoot * remove omitRoot * fix my items to check for namespace when intercepting * update readme --------- Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
parent
69b81c00ca
commit
d42aa545bb
@ -493,6 +493,7 @@
|
||||
"WCAG",
|
||||
"stackedplot",
|
||||
"Andale",
|
||||
"unnnormalized",
|
||||
"checksnapshots",
|
||||
"specced"
|
||||
],
|
||||
|
@ -354,6 +354,9 @@ export default class ObjectAPI {
|
||||
isPersistable(idOrKeyString) {
|
||||
let identifier = utils.parseKeyString(idOrKeyString);
|
||||
let provider = this.getProvider(identifier);
|
||||
if (provider?.isReadOnly) {
|
||||
return !provider.isReadOnly();
|
||||
}
|
||||
|
||||
return provider !== undefined && provider.create !== undefined && provider.update !== undefined;
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ describe('the plugin', function () {
|
||||
|
||||
let couchPlugin = openmct.plugins.CouchDB(testPath);
|
||||
openmct.install(couchPlugin);
|
||||
|
||||
openmct.install(
|
||||
new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, {
|
||||
selector: {
|
||||
|
@ -74,6 +74,10 @@ export default class LocalStorageObjectProvider {
|
||||
this.localStorage.setItem(this.spaceKey, JSON.stringify(space));
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
@ -1,8 +1,26 @@
|
||||
# My Items plugin
|
||||
Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a
|
||||
read-only deployment with no user-editable objects.
|
||||
Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a read-only deployment with no user-editable objects.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
```
|
||||
|
||||
## Options
|
||||
When installing, the plugin can take several options:
|
||||
|
||||
- `name`: The label of the root object. Defaults to "My Items"
|
||||
- Example: `'Apple Items'`
|
||||
|
||||
- `namespace`: The namespace to create the root object in. Defaults to the empty string `''`
|
||||
- Example: `'apple-namespace'`
|
||||
|
||||
- `priority`: The optional priority to install this plugin. Defaults to `openmct.priority.LOW`
|
||||
- Example: `'openmct.priority.LOW'`
|
||||
|
||||
E.g., to install with a custom name and namespace, you could use:
|
||||
|
||||
|
||||
```js
|
||||
openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));
|
||||
```
|
@ -20,9 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { MY_ITEMS_KEY } from './createMyItemsIdentifier.js';
|
||||
|
||||
function myItemsInterceptor(openmct, identifierObject, name) {
|
||||
function myItemsInterceptor({ openmct, identifierObject, name }) {
|
||||
const myItemsModel = {
|
||||
identifier: identifierObject,
|
||||
name,
|
||||
@ -33,7 +31,10 @@ function myItemsInterceptor(openmct, identifierObject, name) {
|
||||
|
||||
return {
|
||||
appliesTo: (identifier) => {
|
||||
return identifier.key === MY_ITEMS_KEY;
|
||||
return (
|
||||
identifier.key === myItemsModel.identifier.key &&
|
||||
identifier.namespace === myItemsModel.identifier.namespace
|
||||
);
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (!object || openmct.objects.isMissing(object)) {
|
||||
|
@ -31,13 +31,13 @@ export default function MyItemsPlugin(
|
||||
priority = undefined
|
||||
) {
|
||||
return function install(openmct) {
|
||||
const identifier = createMyItemsIdentifier(namespace);
|
||||
const identifierObject = createMyItemsIdentifier(namespace);
|
||||
|
||||
if (priority === undefined) {
|
||||
priority = openmct.priority.LOW;
|
||||
}
|
||||
|
||||
openmct.objects.addGetInterceptor(myItemsInterceptor(openmct, identifier, name));
|
||||
openmct.objects.addRoot(identifier, priority);
|
||||
openmct.objects.addGetInterceptor(myItemsInterceptor({ openmct, identifierObject, name }));
|
||||
openmct.objects.addRoot(identifierObject, priority);
|
||||
};
|
||||
}
|
||||
|
@ -80,7 +80,7 @@
|
||||
keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);
|
||||
|
||||
if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {
|
||||
console.debug('⇿ Opening CouchDB change feed connection ⇿');
|
||||
console.debug(`⇿ Opening CouchDB change feed connection for ${changesFeedUrl} ⇿`);
|
||||
couchEventSource = new EventSource(changesFeedUrl);
|
||||
couchEventSource.onerror = self.onerror;
|
||||
couchEventSource.onopen = self.onopen;
|
||||
@ -88,7 +88,7 @@
|
||||
// start listening for events
|
||||
couchEventSource.addEventListener('message', self.onCouchMessage);
|
||||
connected = true;
|
||||
console.debug('⇿ Opened connection ⇿');
|
||||
console.debug(`⇿ Opened connection to ${changesFeedUrl} ⇿`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -33,13 +33,13 @@ const HEARTBEAT = 50000;
|
||||
const ALL_DOCS = '_all_docs?include_docs=true';
|
||||
|
||||
class CouchObjectProvider {
|
||||
constructor(openmct, options, namespace, indicator) {
|
||||
options = this.#normalize(options);
|
||||
constructor({ openmct, databaseConfiguration, couchStatusIndicator }) {
|
||||
this.openmct = openmct;
|
||||
this.indicator = indicator;
|
||||
this.url = options.url;
|
||||
this.useDesignDocuments = options.useDesignDocuments;
|
||||
this.namespace = namespace;
|
||||
this.indicator = couchStatusIndicator;
|
||||
this.url = databaseConfiguration.url;
|
||||
this.readOnly = databaseConfiguration.readOnly;
|
||||
this.useDesignDocuments = databaseConfiguration.useDesignDocuments;
|
||||
this.namespace = databaseConfiguration.namespace;
|
||||
this.objectQueue = {};
|
||||
this.observers = {};
|
||||
this.batchIds = [];
|
||||
@ -47,6 +47,7 @@ class CouchObjectProvider {
|
||||
this.onEventError = this.onEventError.bind(this);
|
||||
this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));
|
||||
this.persistenceQueue = [];
|
||||
this.rootObject = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,7 +60,10 @@ class CouchObjectProvider {
|
||||
// eslint-disable-next-line no-undef
|
||||
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
|
||||
|
||||
sharedWorker = new SharedWorker(sharedWorkerURL, 'CouchDB SSE Shared Worker');
|
||||
sharedWorker = new SharedWorker(
|
||||
sharedWorkerURL,
|
||||
`CouchDB SSE Shared Worker for ${this.namespace}`
|
||||
);
|
||||
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
|
||||
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
|
||||
sharedWorker.port.start();
|
||||
@ -93,7 +97,7 @@ class CouchObjectProvider {
|
||||
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
||||
} else if (event.data.type === 'state') {
|
||||
const state = this.#messageToIndicatorState(event.data.state);
|
||||
this.indicator.setIndicatorToState(state);
|
||||
this.indicator?.setIndicatorToState(state);
|
||||
} else {
|
||||
let objectChanges = event.data.objectChanges;
|
||||
const objectIdentifier = {
|
||||
@ -184,16 +188,8 @@ class CouchObjectProvider {
|
||||
return state;
|
||||
}
|
||||
|
||||
//backwards compatibility, options used to be a url. Now it's an object
|
||||
#normalize(options) {
|
||||
if (typeof options === 'string') {
|
||||
return {
|
||||
url: options,
|
||||
useDesignDocuments: false
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
isReadOnly() {
|
||||
return this.readOnly;
|
||||
}
|
||||
|
||||
async request(subPath, method, body, signal) {
|
||||
@ -233,7 +229,7 @@ class CouchObjectProvider {
|
||||
|
||||
// Network error, CouchDB unreachable.
|
||||
if (response === null) {
|
||||
this.indicator.setIndicatorToState(DISCONNECTED);
|
||||
this.indicator?.setIndicatorToState(DISCONNECTED);
|
||||
console.error(error.message);
|
||||
|
||||
throw new Error(`CouchDB Error - No response"`);
|
||||
@ -256,7 +252,7 @@ class CouchObjectProvider {
|
||||
* @private
|
||||
*/
|
||||
#handleResponseCode(status, json, fetchOptions) {
|
||||
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
|
||||
this.indicator?.setIndicatorToState(this.#statusCodeToIndicatorState(status));
|
||||
if (status === CouchObjectProvider.HTTP_CONFLICT) {
|
||||
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
|
||||
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
|
||||
@ -684,7 +680,7 @@ class CouchObjectProvider {
|
||||
}
|
||||
|
||||
const indicatorState = this.#messageToIndicatorState(message);
|
||||
this.indicator.setIndicatorToState(indicatorState);
|
||||
this.indicator?.setIndicatorToState(indicatorState);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,6 +51,10 @@ class CouchSearchProvider {
|
||||
return this.supportedSearchTypes.includes(searchType);
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
search(query, abortSignal, searchType) {
|
||||
if (searchType === this.searchTypes.OBJECTS) {
|
||||
return this.searchForObjects(query, abortSignal);
|
||||
|
@ -153,13 +153,63 @@ sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.s
|
||||
Add a line to install the CouchDB plugin for Open MCT:
|
||||
|
||||
```js
|
||||
openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: false}));
|
||||
openmct.install(
|
||||
openmct.plugins.CouchDB({
|
||||
databases: [
|
||||
{
|
||||
url: 'http://localhost:5984/openmct',
|
||||
namespace: '',
|
||||
additionalNamespaces: [],
|
||||
readOnly: false,
|
||||
useDesignDocuments: false,
|
||||
indicator: true
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Configuration Options for OpenMCT
|
||||
|
||||
When installing the CouchDB plugin for OpenMCT, you can specify a list of databases with configuration options for each. Here's a breakdown of the available options for each database:
|
||||
|
||||
- `url`: The URL to the CouchDB instance, specifying the protocol, hostname, and port as needed.
|
||||
- Example: `'http://localhost:5984/openmct'`
|
||||
|
||||
- `namespace`: The namespace associated with this database.
|
||||
- Example: `'openmct-sandbox'`
|
||||
|
||||
- `additionalNamespaces`: Other namespaces that this plugin should respond to requests for.
|
||||
- Example: `['apple-namespace', 'pear-namespace']`
|
||||
|
||||
- `readOnly`: A boolean indicating whether the database should be treated as read-only. If set to `true`, OpenMCT will not attempt to write to this database.
|
||||
- Example: `false`
|
||||
|
||||
- `useDesignDocuments`: Indicates whether design documents should be used to speed up annotation search.
|
||||
- Example: `false`
|
||||
|
||||
- `indicator`: A boolean to specify whether an indicator should show the status of this CouchDB connection in the OpenMCT interface.
|
||||
- Example: `true`
|
||||
|
||||
Note: If using the `exampleTags` plugin with non-blank namespaces, you'll need to configure it point to a writable database. For example:
|
||||
|
||||
```js
|
||||
openmct.install(
|
||||
openmct.plugins.example.ExampleTags({ namespaceToSaveAnnotations: 'openmct-sandbox' })
|
||||
);
|
||||
```
|
||||
|
||||
Note: If using the `MyItems` plugin, be sure to configure a root for each writable namespace. E.g., if you have two namespaces called `apple-namespace` and `pear-namespace`:
|
||||
```js
|
||||
openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));
|
||||
openmct.install(openmct.plugins.MyItems('Pear Items', 'pear-namespace'));
|
||||
```
|
||||
This will create a root object with the id of `mine` in both namespaces upon load if not already created.
|
||||
|
||||
# Validating a successful Installation
|
||||
|
||||
1. Start Open MCT by running `npm start` in the `openmct` path.
|
||||
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
|
||||
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save.
|
||||
3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
|
||||
4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
|
||||
5. All done! 🏆
|
||||
@ -242,4 +292,4 @@ To enable them in Open MCT, we need to configure the plugin `useDesignDocuments`
|
||||
|
||||
```js
|
||||
openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: true}));
|
||||
```
|
||||
```
|
||||
|
@ -24,29 +24,81 @@ import CouchObjectProvider from './CouchObjectProvider.js';
|
||||
import CouchSearchProvider from './CouchSearchProvider.js';
|
||||
import CouchStatusIndicator from './CouchStatusIndicator.js';
|
||||
|
||||
const NAMESPACE = '';
|
||||
const DEFAULT_NAMESPACE = '';
|
||||
const LEGACY_SPACE = 'mct';
|
||||
const COUCH_SEARCH_ONLY_NAMESPACE = `COUCH_SEARCH_${Date.now()}`;
|
||||
|
||||
export default function CouchPlugin(options) {
|
||||
return function install(openmct) {
|
||||
const simpleIndicator = openmct.indicators.simpleIndicator();
|
||||
openmct.indicators.add(simpleIndicator);
|
||||
const couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
|
||||
install.couchProvider = new CouchObjectProvider(
|
||||
openmct,
|
||||
options,
|
||||
NAMESPACE,
|
||||
couchStatusIndicator
|
||||
);
|
||||
function normalizeOptions(unnnormalizedOptions) {
|
||||
const normalizedOptions = {};
|
||||
if (typeof unnnormalizedOptions === 'string') {
|
||||
normalizedOptions.databases = [
|
||||
{
|
||||
url: options,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
additionalNamespaces: [LEGACY_SPACE],
|
||||
readOnly: false,
|
||||
useDesignDocuments: false,
|
||||
indicator: true
|
||||
}
|
||||
];
|
||||
} else if (!unnnormalizedOptions.databases) {
|
||||
normalizedOptions.databases = [
|
||||
{
|
||||
url: unnnormalizedOptions.url,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
additionalNamespaces: [LEGACY_SPACE],
|
||||
readOnly: false,
|
||||
useDesignDocuments: unnnormalizedOptions.useDesignDocuments,
|
||||
indicator: true
|
||||
}
|
||||
];
|
||||
} else {
|
||||
normalizedOptions.databases = unnnormalizedOptions.databases;
|
||||
}
|
||||
|
||||
// Unfortunately, for historical reasons, Couch DB produces objects with a mix of namespaces (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);
|
||||
openmct.objects.addProvider(
|
||||
COUCH_SEARCH_ONLY_NAMESPACE,
|
||||
new CouchSearchProvider(install.couchProvider)
|
||||
);
|
||||
// final sanity check, ensure we have all options
|
||||
normalizedOptions.databases.forEach((databaseConfiguration) => {
|
||||
if (!databaseConfiguration.url) {
|
||||
throw new Error(
|
||||
`🛑 CouchDB plugin requires a url option. Please check the configuration for namespace ${databaseConfiguration.namespace}`
|
||||
);
|
||||
} else if (databaseConfiguration.namespace === undefined) {
|
||||
// note we can't check for just !databaseConfiguration.namespace because it could be an empty string
|
||||
throw new Error(
|
||||
`🛑 CouchDB plugin requires a namespace option. Please check the configuration for url ${databaseConfiguration.url}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return normalizedOptions;
|
||||
}
|
||||
|
||||
return function install(openmct) {
|
||||
const normalizedOptions = normalizeOptions(options);
|
||||
normalizedOptions.databases.forEach((databaseConfiguration) => {
|
||||
let couchStatusIndicator;
|
||||
if (databaseConfiguration.indicator) {
|
||||
const simpleIndicator = openmct.indicators.simpleIndicator();
|
||||
openmct.indicators.add(simpleIndicator);
|
||||
couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
|
||||
}
|
||||
// the provider is added to the install function to expose couchProvider to unit tests
|
||||
install.couchProvider = new CouchObjectProvider({
|
||||
openmct,
|
||||
databaseConfiguration,
|
||||
couchStatusIndicator
|
||||
});
|
||||
openmct.objects.addProvider(databaseConfiguration.namespace, install.couchProvider);
|
||||
databaseConfiguration.additionalNamespaces?.forEach((additionalNamespace) => {
|
||||
openmct.objects.addProvider(additionalNamespace, install.couchProvider);
|
||||
});
|
||||
|
||||
// need one search provider for whole couch database
|
||||
const searchOnlyNamespace = `COUCH_SEARCH_${databaseConfiguration.namespace}${Date.now()}`;
|
||||
openmct.objects.addProvider(
|
||||
searchOnlyNamespace,
|
||||
new CouchSearchProvider(install.couchProvider)
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user