Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple CouchDB databases, multiple namespaces, and readOnly configurations #7413

Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@
"WCAG",
"stackedplot",
"Andale",
"unnnormalized",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm prepared to accept unnormalized at a stretch. But unnnormalized? With three n's?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

"checksnapshots",
"specced"
],
Expand Down
3 changes: 3 additions & 0 deletions src/api/objects/ObjectAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 0 additions & 1 deletion src/plugins/CouchDBSearchFolder/pluginSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/localStorage/LocalStorageObjectProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export default class LocalStorageObjectProvider {
this.localStorage.setItem(this.spaceKey, JSON.stringify(space));
}

isReadOnly() {
return false;
}

/**
* @private
*/
Expand Down
22 changes: 20 additions & 2 deletions src/plugins/myItems/README.md
Original file line number Diff line number Diff line change
@@ -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'));
```
9 changes: 5 additions & 4 deletions src/plugins/myItems/myItemsInterceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)) {
Expand Down
6 changes: 3 additions & 3 deletions src/plugins/myItems/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
4 changes: 2 additions & 2 deletions src/plugins/persistence/couch/CouchChangesFeed.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@
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;

// start listening for events
couchEventSource.addEventListener('message', self.onCouchMessage);
connected = true;
console.debug('⇿ Opened connection ⇿');
console.debug(`⇿ Opened connection to ${changesFeedUrl} ⇿`);
}
};

Expand Down
38 changes: 17 additions & 21 deletions src/plugins/persistence/couch/CouchObjectProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,21 @@ 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 = [];
this.onEventMessage = this.onEventMessage.bind(this);
this.onEventError = this.onEventError.bind(this);
this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));
this.persistenceQueue = [];
this.rootObject = null;
}

/**
Expand All @@ -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();
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"`);
Expand All @@ -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}"`);
Expand Down Expand Up @@ -684,7 +680,7 @@ class CouchObjectProvider {
}

const indicatorState = this.#messageToIndicatorState(message);
this.indicator.setIndicatorToState(indicatorState);
this.indicator?.setIndicatorToState(indicatorState);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/persistence/couch/CouchSearchProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
return this.supportedSearchTypes.includes(searchType);
}

isReadOnly() {
return true;
}

Check warning on line 56 in src/plugins/persistence/couch/CouchSearchProvider.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/persistence/couch/CouchSearchProvider.js#L55-L56

Added lines #L55 - L56 were not covered by tests

search(query, abortSignal, searchType) {
if (searchType === this.searchTypes.OBJECTS) {
return this.searchForObjects(query, abortSignal);
Expand Down
56 changes: 53 additions & 3 deletions src/plugins/persistence/couch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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! 🏆
Expand Down Expand Up @@ -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}));
```
```
Loading
Loading