mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 05:07:52 +00:00
Implement Couch Search and Request Batching (#3798)
* Implemented search in couch provider * Promises, not await * Batch requests * Only batch if > 1 * Remove legacy Couch adapter * Cleaned up couch request batching code * Added test cases * Code cleanup * Changes to new and legacy objects API to remove redundant persists due to mutation of modified and persisted timestamps * Cleaned up couch unit tests Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
This commit is contained in:
parent
9fa71244ea
commit
564f254652
@ -1,8 +0,0 @@
|
|||||||
# Couch DB Persistence Plugin
|
|
||||||
An adapter for using CouchDB for persistence of user-created objects. The plugin installation function takes the URL
|
|
||||||
for the CouchDB database as a parameter.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
```js
|
|
||||||
openmct.install(openmct.plugins.CouchDB('http://localhost:5984/openmct'))
|
|
||||||
```
|
|
@ -1,78 +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/CouchPersistenceProvider",
|
|
||||||
"./src/CouchIndicator"
|
|
||||||
], function (
|
|
||||||
CouchPersistenceProvider,
|
|
||||||
CouchIndicator
|
|
||||||
) {
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "platform/persistence/couch",
|
|
||||||
definition: {
|
|
||||||
"name": "Couch Persistence",
|
|
||||||
"description": "Adapter to read and write objects using a CouchDB instance.",
|
|
||||||
"extensions": {
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"provides": "persistenceService",
|
|
||||||
"type": "provider",
|
|
||||||
"implementation": CouchPersistenceProvider,
|
|
||||||
"depends": [
|
|
||||||
"$http",
|
|
||||||
"$q",
|
|
||||||
"PERSISTENCE_SPACE",
|
|
||||||
"COUCHDB_PATH"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"constants": [
|
|
||||||
{
|
|
||||||
"key": "PERSISTENCE_SPACE",
|
|
||||||
"value": "mct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "COUCHDB_PATH",
|
|
||||||
"value": "/couch/openmct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "COUCHDB_INDICATOR_INTERVAL",
|
|
||||||
"value": 15000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indicators": [
|
|
||||||
{
|
|
||||||
"implementation": CouchIndicator,
|
|
||||||
"depends": [
|
|
||||||
"$http",
|
|
||||||
"$interval",
|
|
||||||
"COUCHDB_PATH",
|
|
||||||
"COUCHDB_INDICATOR_INTERVAL"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
@ -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 () {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A CouchDocument describes domain object model in a format
|
|
||||||
* which is easily read-written to CouchDB. This includes
|
|
||||||
* Couch's _id and _rev fields, as well as a separate
|
|
||||||
* metadata field which contains a subset of information found
|
|
||||||
* in the model itself (to support search optimization with
|
|
||||||
* CouchDB views.)
|
|
||||||
* @memberof platform/persistence/couch
|
|
||||||
* @constructor
|
|
||||||
* @param {string} id the id under which to store this mode
|
|
||||||
* @param {object} model the model to store
|
|
||||||
* @param {string} rev the revision to include (or undefined,
|
|
||||||
* if no revision should be noted for couch)
|
|
||||||
* @param {boolean} whether or not to mark this document as
|
|
||||||
* deleted (see CouchDB docs for _deleted)
|
|
||||||
*/
|
|
||||||
function CouchDocument(id, model, rev, markDeleted) {
|
|
||||||
return {
|
|
||||||
"_id": id,
|
|
||||||
"_rev": rev,
|
|
||||||
"_deleted": markDeleted,
|
|
||||||
"metadata": {
|
|
||||||
"category": "domain object",
|
|
||||||
"type": model.type,
|
|
||||||
"owner": "admin",
|
|
||||||
"name": model.name,
|
|
||||||
"created": Date.now()
|
|
||||||
},
|
|
||||||
"model": model
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return CouchDocument;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,119 +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 () {
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// SEMICONNECTED: Connected to the database, but it reported an error.
|
|
||||||
// PENDING: Still trying to connect, and haven't failed yet.
|
|
||||||
var CONNECTED = {
|
|
||||||
text: "Connected",
|
|
||||||
glyphClass: "ok",
|
|
||||||
statusClass: "s-status-on",
|
|
||||||
description: "Connected to the domain object database."
|
|
||||||
},
|
|
||||||
DISCONNECTED = {
|
|
||||||
text: "Disconnected",
|
|
||||||
glyphClass: "err",
|
|
||||||
statusClass: "s-status-caution",
|
|
||||||
description: "Unable to connect to the domain object database."
|
|
||||||
},
|
|
||||||
SEMICONNECTED = {
|
|
||||||
text: "Unavailable",
|
|
||||||
glyphClass: "caution",
|
|
||||||
statusClass: "s-status-caution",
|
|
||||||
description: "Database does not exist or is unavailable."
|
|
||||||
},
|
|
||||||
PENDING = {
|
|
||||||
text: "Checking connection...",
|
|
||||||
statusClass: "s-status-caution"
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicator for the current CouchDB connection. Polls CouchDB
|
|
||||||
* at a regular interval (defined by bundle constants) to ensure
|
|
||||||
* that the database is available.
|
|
||||||
* @constructor
|
|
||||||
* @memberof platform/persistence/couch
|
|
||||||
* @implements {Indicator}
|
|
||||||
* @param $http Angular's $http service
|
|
||||||
* @param $interval Angular's $interval service
|
|
||||||
* @param {string} path the URL to poll to check for couch availability
|
|
||||||
* @param {number} interval the interval, in milliseconds, to poll at
|
|
||||||
*/
|
|
||||||
function CouchIndicator($http, $interval, path, interval) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Track the current connection state
|
|
||||||
this.state = PENDING;
|
|
||||||
|
|
||||||
this.$http = $http;
|
|
||||||
this.$interval = $interval;
|
|
||||||
this.path = path;
|
|
||||||
this.interval = interval;
|
|
||||||
|
|
||||||
// Callback if the HTTP request to Couch fails
|
|
||||||
function handleError() {
|
|
||||||
self.state = DISCONNECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback if the HTTP request succeeds. CouchDB may
|
|
||||||
// report an error, so check for that.
|
|
||||||
function handleResponse(response) {
|
|
||||||
var data = response.data;
|
|
||||||
self.state = data.error ? SEMICONNECTED : 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
CouchIndicator.prototype.getCssClass = function () {
|
|
||||||
return "c-indicator--clickable icon-suitcase " + this.state.statusClass;
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchIndicator.prototype.getGlyphClass = function () {
|
|
||||||
return this.state.glyphClass;
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchIndicator.prototype.getText = function () {
|
|
||||||
return this.state.text;
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchIndicator.prototype.getDescription = function () {
|
|
||||||
return this.state.description;
|
|
||||||
};
|
|
||||||
|
|
||||||
return CouchIndicator;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,145 +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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This bundle implements a persistence service which uses CouchDB to
|
|
||||||
* store documents.
|
|
||||||
* @namespace platform/persistence/cache
|
|
||||||
*/
|
|
||||||
define(
|
|
||||||
["./CouchDocument"],
|
|
||||||
function (CouchDocument) {
|
|
||||||
|
|
||||||
// JSLint doesn't like dangling _'s, but CouchDB uses these, so
|
|
||||||
// hide this behind variables.
|
|
||||||
var REV = "_rev",
|
|
||||||
ID = "_id";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The CouchPersistenceProvider reads and writes JSON documents
|
|
||||||
* (more specifically, domain object models) to/from a CouchDB
|
|
||||||
* instance.
|
|
||||||
* @memberof platform/persistence/couch
|
|
||||||
* @constructor
|
|
||||||
* @implements {PersistenceService}
|
|
||||||
* @param $http Angular's $http service
|
|
||||||
* @param $interval Angular's $interval service
|
|
||||||
* @param {string} space the name of the persistence space being served
|
|
||||||
* @param {string} path the path to the CouchDB instance
|
|
||||||
*/
|
|
||||||
function CouchPersistenceProvider($http, $q, space, path) {
|
|
||||||
this.spaces = [space];
|
|
||||||
this.revs = {};
|
|
||||||
this.$q = $q;
|
|
||||||
this.$http = $http;
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull out a list of document IDs from CouchDB's
|
|
||||||
// _all_docs response
|
|
||||||
function getIdsFromAllDocs(allDocs) {
|
|
||||||
return allDocs.rows.map(function (r) {
|
|
||||||
return r.id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
CouchPersistenceProvider.prototype.checkResponse = function (response) {
|
|
||||||
if (response && response.ok) {
|
|
||||||
this.revs[response.id] = response.rev;
|
|
||||||
|
|
||||||
return response.ok;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get a domain object model out of CouchDB's response
|
|
||||||
CouchPersistenceProvider.prototype.getModel = function (response) {
|
|
||||||
if (response && response.model) {
|
|
||||||
this.revs[response[ID]] = response[REV];
|
|
||||||
|
|
||||||
return response.model;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Issue a request using $http; get back the plain JS object
|
|
||||||
// from the expected JSON response
|
|
||||||
CouchPersistenceProvider.prototype.request = function (subpath, method, value) {
|
|
||||||
return this.$http({
|
|
||||||
method: method,
|
|
||||||
url: this.path + '/' + subpath,
|
|
||||||
data: value
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.data;
|
|
||||||
}, function () {
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shorthand methods for GET/PUT methods
|
|
||||||
CouchPersistenceProvider.prototype.get = function (subpath) {
|
|
||||||
return this.request(subpath, "GET");
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.put = function (subpath, value) {
|
|
||||||
return this.request(subpath, "PUT", value);
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.listSpaces = function () {
|
|
||||||
return this.$q.when(this.spaces);
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.listObjects = function () {
|
|
||||||
return this.get("_all_docs").then(getIdsFromAllDocs.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.createObject = function (space, key, value) {
|
|
||||||
return this.put(key, new CouchDocument(key, value))
|
|
||||||
.then(this.checkResponse.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.readObject = function (space, key) {
|
|
||||||
return this.get(key).then(this.getModel.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.updateObject = function (space, key, value) {
|
|
||||||
var rev = this.revs[key];
|
|
||||||
|
|
||||||
return this.put(key, new CouchDocument(key, value, rev))
|
|
||||||
.then(this.checkResponse.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
CouchPersistenceProvider.prototype.deleteObject = function (space, key, value) {
|
|
||||||
var rev = this.revs[key];
|
|
||||||
|
|
||||||
return this.put(key, new CouchDocument(key, value, rev, true))
|
|
||||||
.then(this.checkResponse.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
return CouchPersistenceProvider;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,63 +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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DomainObjectProviderSpec. Created by vwoeltje on 11/6/14.
|
|
||||||
*/
|
|
||||||
define(
|
|
||||||
["../src/CouchDocument"],
|
|
||||||
function (CouchDocument) {
|
|
||||||
|
|
||||||
// JSLint doesn't like dangling _'s, but CouchDB uses these, so
|
|
||||||
// hide this behind variables.
|
|
||||||
var REV = "_rev",
|
|
||||||
ID = "_id",
|
|
||||||
DELETED = "_deleted";
|
|
||||||
|
|
||||||
describe("A couch document", function () {
|
|
||||||
it("includes an id", function () {
|
|
||||||
expect(new CouchDocument("testId", {})[ID])
|
|
||||||
.toEqual("testId");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes a rev only when one is provided", function () {
|
|
||||||
expect(new CouchDocument("testId", {})[REV])
|
|
||||||
.not.toBeDefined();
|
|
||||||
expect(new CouchDocument("testId", {}, "testRev")[REV])
|
|
||||||
.toEqual("testRev");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes the provided model", function () {
|
|
||||||
var model = { someKey: "some value" };
|
|
||||||
expect(new CouchDocument("testId", model).model)
|
|
||||||
.toEqual(model);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("marks documents as deleted only on request", function () {
|
|
||||||
expect(new CouchDocument("testId", {}, "testRev")[DELETED])
|
|
||||||
.not.toBeDefined();
|
|
||||||
expect(new CouchDocument("testId", {}, "testRev", true)[DELETED])
|
|
||||||
.toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,129 +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/CouchIndicator"],
|
|
||||||
function (CouchIndicator) {
|
|
||||||
|
|
||||||
xdescribe("The CouchDB 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.and.returnValue(mockPromise);
|
|
||||||
|
|
||||||
indicator = new CouchIndicator(
|
|
||||||
mockHttp,
|
|
||||||
mockInterval,
|
|
||||||
testPath,
|
|
||||||
testInterval
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("polls for changes", function () {
|
|
||||||
expect(mockInterval).toHaveBeenCalledWith(
|
|
||||||
jasmine.any(Function),
|
|
||||||
testInterval
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has a database icon", function () {
|
|
||||||
expect(indicator.getCssClass()).toEqual("icon-database s-status-caution");
|
|
||||||
});
|
|
||||||
|
|
||||||
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 object, without
|
|
||||||
// an error field.
|
|
||||||
mockPromise.then.calls.mostRecent().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 reports an error", function () {
|
|
||||||
var initialText = indicator.getText(),
|
|
||||||
initialDescrption = indicator.getDescription(),
|
|
||||||
initialGlyphClass = indicator.getGlyphClass();
|
|
||||||
|
|
||||||
// Nominal just means getting back an object, with
|
|
||||||
// an error field.
|
|
||||||
mockPromise.then.calls.mostRecent().args[0](
|
|
||||||
{ data: { error: "Uh oh." } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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("caution");
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
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 object, without
|
|
||||||
// an error field.
|
|
||||||
mockPromise.then.calls.mostRecent().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");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,223 +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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DomainObjectProviderSpec. Created by vwoeltje on 11/6/14.
|
|
||||||
*/
|
|
||||||
define(
|
|
||||||
["../src/CouchPersistenceProvider"],
|
|
||||||
function (CouchPersistenceProvider) {
|
|
||||||
|
|
||||||
describe("The couch persistence provider", function () {
|
|
||||||
var mockHttp,
|
|
||||||
mockQ,
|
|
||||||
testSpace = "testSpace",
|
|
||||||
testPath = "/test/db",
|
|
||||||
capture,
|
|
||||||
provider;
|
|
||||||
|
|
||||||
function mockPromise(value) {
|
|
||||||
return {
|
|
||||||
then: function (callback) {
|
|
||||||
return mockPromise(callback(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockHttp = jasmine.createSpy("$http");
|
|
||||||
mockQ = jasmine.createSpyObj("$q", ["when"]);
|
|
||||||
|
|
||||||
mockQ.when.and.callFake(mockPromise);
|
|
||||||
|
|
||||||
// Capture promise results
|
|
||||||
capture = jasmine.createSpy("capture");
|
|
||||||
|
|
||||||
provider = new CouchPersistenceProvider(
|
|
||||||
mockHttp,
|
|
||||||
mockQ,
|
|
||||||
testSpace,
|
|
||||||
testPath
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports available spaces", function () {
|
|
||||||
provider.listSpaces().then(capture);
|
|
||||||
expect(capture).toHaveBeenCalledWith([testSpace]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// General pattern of tests below is to simulate CouchDB's
|
|
||||||
// response, verify that request looks like what CouchDB
|
|
||||||
// would expect, and finally verify that CouchPersistenceProvider's
|
|
||||||
// return values match what is expected.
|
|
||||||
it("lists all available documents", function () {
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: { rows: [{ id: "a" }, { id: "b" }, { id: "c" }] }
|
|
||||||
}));
|
|
||||||
provider.listObjects().then(capture);
|
|
||||||
expect(mockHttp).toHaveBeenCalledWith({
|
|
||||||
url: "/test/db/_all_docs", // couch document listing
|
|
||||||
method: "GET",
|
|
||||||
data: undefined
|
|
||||||
});
|
|
||||||
expect(capture).toHaveBeenCalledWith(["a", "b", "c"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows object creation", function () {
|
|
||||||
var model = { someKey: "some value" };
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"ok": true
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.createObject("testSpace", "abc", model).then(capture);
|
|
||||||
expect(mockHttp).toHaveBeenCalledWith({
|
|
||||||
url: "/test/db/abc",
|
|
||||||
method: "PUT",
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": undefined,
|
|
||||||
"_deleted": undefined,
|
|
||||||
metadata: jasmine.any(Object),
|
|
||||||
model: model
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(capture).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows object models to be read back", function () {
|
|
||||||
var model = { someKey: "some value" };
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"model": model
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.readObject("testSpace", "abc").then(capture);
|
|
||||||
expect(mockHttp).toHaveBeenCalledWith({
|
|
||||||
url: "/test/db/abc",
|
|
||||||
method: "GET",
|
|
||||||
data: undefined
|
|
||||||
});
|
|
||||||
expect(capture).toHaveBeenCalledWith(model);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows object update", function () {
|
|
||||||
var model = { someKey: "some value" };
|
|
||||||
|
|
||||||
// First do a read to populate rev tags...
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"model": {}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.readObject("testSpace", "abc");
|
|
||||||
|
|
||||||
// Now perform an update
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "uvw",
|
|
||||||
"ok": true
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.updateObject("testSpace", "abc", model).then(capture);
|
|
||||||
expect(mockHttp).toHaveBeenCalledWith({
|
|
||||||
url: "/test/db/abc",
|
|
||||||
method: "PUT",
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"_deleted": undefined,
|
|
||||||
metadata: jasmine.any(Object),
|
|
||||||
model: model
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(capture).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows object deletion", function () {
|
|
||||||
// First do a read to populate rev tags...
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"model": {}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.readObject("testSpace", "abc");
|
|
||||||
|
|
||||||
// Now perform an update
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "uvw",
|
|
||||||
"ok": true
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.deleteObject("testSpace", "abc", {}).then(capture);
|
|
||||||
expect(mockHttp).toHaveBeenCalledWith({
|
|
||||||
url: "/test/db/abc",
|
|
||||||
method: "PUT",
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"_deleted": true,
|
|
||||||
metadata: jasmine.any(Object),
|
|
||||||
model: {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(capture).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports failure to create objects", function () {
|
|
||||||
var model = { someKey: "some value" };
|
|
||||||
mockHttp.and.returnValue(mockPromise({
|
|
||||||
data: {
|
|
||||||
"_id": "abc",
|
|
||||||
"_rev": "xyz",
|
|
||||||
"ok": false
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
provider.createObject("testSpace", "abc", model).then(capture);
|
|
||||||
expect(capture).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns undefined when objects are not found", function () {
|
|
||||||
// Act like a 404
|
|
||||||
mockHttp.and.returnValue({
|
|
||||||
then: function (success, fail) {
|
|
||||||
return mockPromise(fail());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
provider.readObject("testSpace", "abc").then(capture);
|
|
||||||
expect(capture).toHaveBeenCalledWith(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -52,7 +52,7 @@ define([
|
|||||||
|
|
||||||
oldStyleObject.getCapability('mutation').mutate(function () {
|
oldStyleObject.getCapability('mutation').mutate(function () {
|
||||||
return utils.toOldFormat(newStyleObject);
|
return utils.toOldFormat(newStyleObject);
|
||||||
});
|
}, newStyleObject.modified);
|
||||||
|
|
||||||
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
@ -76,7 +76,10 @@ class MutableDomainObject {
|
|||||||
}
|
}
|
||||||
$set(path, value) {
|
$set(path, value) {
|
||||||
_.set(this, path, value);
|
_.set(this, path, value);
|
||||||
_.set(this, 'modified', Date.now());
|
|
||||||
|
if (path !== 'persisted' && path !== 'modified') {
|
||||||
|
_.set(this, 'modified', Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
|
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
|
||||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
|
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
|
||||||
|
@ -520,8 +520,10 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function hasAlreadyBeenPersisted(domainObject) {
|
function hasAlreadyBeenPersisted(domainObject) {
|
||||||
return domainObject.persisted !== undefined
|
const result = domainObject.persisted !== undefined
|
||||||
&& domainObject.persisted === domainObject.modified;
|
&& domainObject.persisted >= domainObject.modified;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectAPI;
|
export default ObjectAPI;
|
||||||
|
@ -90,7 +90,6 @@ define([
|
|||||||
'../platform/framework/src/load/Bundle',
|
'../platform/framework/src/load/Bundle',
|
||||||
'../platform/identity/bundle',
|
'../platform/identity/bundle',
|
||||||
'../platform/persistence/aggregator/bundle',
|
'../platform/persistence/aggregator/bundle',
|
||||||
'../platform/persistence/couch/bundle',
|
|
||||||
'../platform/persistence/elastic/bundle',
|
'../platform/persistence/elastic/bundle',
|
||||||
'../platform/persistence/local/bundle',
|
'../platform/persistence/local/bundle',
|
||||||
'../platform/persistence/queue/bundle',
|
'../platform/persistence/queue/bundle',
|
||||||
|
@ -26,6 +26,7 @@ import CouchObjectQueue from "./CouchObjectQueue";
|
|||||||
const REV = "_rev";
|
const REV = "_rev";
|
||||||
const ID = "_id";
|
const ID = "_id";
|
||||||
const HEARTBEAT = 50000;
|
const HEARTBEAT = 50000;
|
||||||
|
const ALL_DOCS = "_all_docs?include_docs=true";
|
||||||
|
|
||||||
export default class CouchObjectProvider {
|
export default class CouchObjectProvider {
|
||||||
// options {
|
// options {
|
||||||
@ -41,6 +42,8 @@ export default class CouchObjectProvider {
|
|||||||
this.objectQueue = {};
|
this.objectQueue = {};
|
||||||
this.observeEnabled = options.disableObserve !== true;
|
this.observeEnabled = options.disableObserve !== true;
|
||||||
this.observers = {};
|
this.observers = {};
|
||||||
|
this.batchIds = [];
|
||||||
|
|
||||||
if (this.observeEnabled) {
|
if (this.observeEnabled) {
|
||||||
this.observeObjectChanges(options.filter);
|
this.observeObjectChanges(options.filter);
|
||||||
}
|
}
|
||||||
@ -67,6 +70,9 @@ export default class CouchObjectProvider {
|
|||||||
// stringify body if needed
|
// stringify body if needed
|
||||||
if (fetchOptions.body) {
|
if (fetchOptions.body) {
|
||||||
fetchOptions.body = JSON.stringify(fetchOptions.body);
|
fetchOptions.body = JSON.stringify(fetchOptions.body);
|
||||||
|
fetchOptions.headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(this.url + '/' + subPath, fetchOptions)
|
return fetch(this.url + '/' + subPath, fetchOptions)
|
||||||
@ -78,14 +84,18 @@ export default class CouchObjectProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the response to a create/update/delete request;
|
/**
|
||||||
// track the rev if it's valid, otherwise return false to
|
* Check the response to a create/update/delete request;
|
||||||
// indicate that the request failed.
|
* track the rev if it's valid, otherwise return false to
|
||||||
// persist any queued objects
|
* indicate that the request failed.
|
||||||
|
* persist any queued objects
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
checkResponse(response, intermediateResponse) {
|
checkResponse(response, intermediateResponse) {
|
||||||
let requestSuccess = false;
|
let requestSuccess = false;
|
||||||
const id = response ? response.id : undefined;
|
const id = response ? response.id : undefined;
|
||||||
let rev;
|
let rev;
|
||||||
|
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
rev = response.rev;
|
rev = response.rev;
|
||||||
requestSuccess = true;
|
requestSuccess = true;
|
||||||
@ -106,6 +116,9 @@ export default class CouchObjectProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
getModel(response) {
|
getModel(response) {
|
||||||
if (response && response.model) {
|
if (response && response.model) {
|
||||||
let key = response[ID];
|
let key = response[ID];
|
||||||
@ -131,10 +144,118 @@ export default class CouchObjectProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(identifier, abortSignal) {
|
get(identifier, abortSignal) {
|
||||||
return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
|
this.batchIds.push(identifier.key);
|
||||||
|
|
||||||
|
if (this.bulkPromise === undefined) {
|
||||||
|
this.bulkPromise = this.deferBatchedGet(abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.bulkPromise
|
||||||
|
.then((domainObjectMap) => {
|
||||||
|
return domainObjectMap[identifier.key];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getObjectsByFilter(filter) {
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
deferBatchedGet(abortSignal) {
|
||||||
|
// We until the next event loop cycle to "collect" all of the get
|
||||||
|
// requests triggered in this iteration of the event loop
|
||||||
|
|
||||||
|
return this.waitOneEventCycle().then(() => {
|
||||||
|
let batchIds = this.batchIds;
|
||||||
|
|
||||||
|
this.clearBatch();
|
||||||
|
|
||||||
|
if (batchIds.length === 1) {
|
||||||
|
let objectKey = batchIds[0];
|
||||||
|
|
||||||
|
//If there's only one request, just do a regular get
|
||||||
|
return this.request(objectKey, "GET", undefined, abortSignal)
|
||||||
|
.then(this.returnAsMap(objectKey));
|
||||||
|
} else {
|
||||||
|
return this.bulkGet(batchIds, abortSignal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
returnAsMap(objectKey) {
|
||||||
|
return (result) => {
|
||||||
|
let objectMap = {};
|
||||||
|
objectMap[objectKey] = this.getModel(result);
|
||||||
|
|
||||||
|
return objectMap;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
clearBatch() {
|
||||||
|
this.batchIds = [];
|
||||||
|
delete this.bulkPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
waitOneEventCycle() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
bulkGet(ids, signal) {
|
||||||
|
ids = this.removeDuplicates(ids);
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
'keys': ids
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
|
||||||
|
if (response && response.rows !== undefined) {
|
||||||
|
return response.rows.reduce((map, row) => {
|
||||||
|
if (row.doc !== undefined) {
|
||||||
|
map[row.key] = this.getModel(row.doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
removeDuplicates(array) {
|
||||||
|
return Array.from(new Set(array));
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query, abortSignal) {
|
||||||
|
const filter = {
|
||||||
|
"selector": {
|
||||||
|
"model": {
|
||||||
|
"name": {
|
||||||
|
"$regex": `(?i)${query}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.getObjectsByFilter(filter, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObjectsByFilter(filter, abortSignal) {
|
||||||
let objects = [];
|
let objects = [];
|
||||||
|
|
||||||
let url = `${this.url}/_find`;
|
let url = `${this.url}/_find`;
|
||||||
@ -149,6 +270,7 @@ export default class CouchObjectProvider {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
|
signal: abortSignal,
|
||||||
body
|
body
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -203,6 +325,9 @@ export default class CouchObjectProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
abortGetChanges() {
|
abortGetChanges() {
|
||||||
if (this.controller) {
|
if (this.controller) {
|
||||||
this.controller.abort();
|
this.controller.abort();
|
||||||
@ -212,6 +337,9 @@ export default class CouchObjectProvider {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
async observeObjectChanges(filter) {
|
async observeObjectChanges(filter) {
|
||||||
let intermediateResponse = this.getIntermediateResponse();
|
let intermediateResponse = this.getIntermediateResponse();
|
||||||
|
|
||||||
@ -292,6 +420,9 @@ export default class CouchObjectProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
getIntermediateResponse() {
|
getIntermediateResponse() {
|
||||||
let intermediateResponse = {};
|
let intermediateResponse = {};
|
||||||
intermediateResponse.promise = new Promise(function (resolve, reject) {
|
intermediateResponse.promise = new Promise(function (resolve, reject) {
|
||||||
@ -302,6 +433,9 @@ export default class CouchObjectProvider {
|
|||||||
return intermediateResponse;
|
return intermediateResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
enqueueObject(key, model, intermediateResponse) {
|
enqueueObject(key, model, intermediateResponse) {
|
||||||
if (this.objectQueue[key]) {
|
if (this.objectQueue[key]) {
|
||||||
this.objectQueue[key].enqueue({
|
this.objectQueue[key].enqueue({
|
||||||
@ -330,6 +464,9 @@ export default class CouchObjectProvider {
|
|||||||
return intermediateResponse.promise;
|
return intermediateResponse.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
updateQueued(key) {
|
updateQueued(key) {
|
||||||
if (!this.objectQueue[key].pending) {
|
if (!this.objectQueue[key].pending) {
|
||||||
this.objectQueue[key].pending = true;
|
this.objectQueue[key].pending = true;
|
||||||
|
@ -24,7 +24,6 @@ import {
|
|||||||
createOpenMct,
|
createOpenMct,
|
||||||
resetApplicationState, spyOnBuiltins
|
resetApplicationState, spyOnBuiltins
|
||||||
} from 'utils/testing';
|
} from 'utils/testing';
|
||||||
import CouchObjectProvider from './CouchObjectProvider';
|
|
||||||
|
|
||||||
describe('the plugin', () => {
|
describe('the plugin', () => {
|
||||||
let openmct;
|
let openmct;
|
||||||
@ -42,7 +41,8 @@ describe('the plugin', () => {
|
|||||||
namespace: '',
|
namespace: '',
|
||||||
key: 'some-value'
|
key: 'some-value'
|
||||||
},
|
},
|
||||||
type: 'mock-type'
|
type: 'mock-type',
|
||||||
|
modified: 0
|
||||||
};
|
};
|
||||||
options = {
|
options = {
|
||||||
url: testPath,
|
url: testPath,
|
||||||
@ -95,6 +95,7 @@ describe('the plugin', () => {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
_id: 'some-value',
|
_id: 'some-value',
|
||||||
|
id: 'some-value',
|
||||||
_rev: 1,
|
_rev: 1,
|
||||||
model: {}
|
model: {}
|
||||||
};
|
};
|
||||||
@ -104,44 +105,130 @@ describe('the plugin', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('gets an object', () => {
|
it('gets an object', () => {
|
||||||
openmct.objects.get(mockDomainObject.identifier).then((result) => {
|
return openmct.objects.get(mockDomainObject.identifier).then((result) => {
|
||||||
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
|
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an object', () => {
|
it('creates an object', () => {
|
||||||
openmct.objects.save(mockDomainObject).then((result) => {
|
return openmct.objects.save(mockDomainObject).then((result) => {
|
||||||
expect(provider.create).toHaveBeenCalled();
|
expect(provider.create).toHaveBeenCalled();
|
||||||
expect(result).toBeTrue();
|
expect(result).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates an object', () => {
|
it('updates an object', () => {
|
||||||
openmct.objects.save(mockDomainObject).then((result) => {
|
return openmct.objects.save(mockDomainObject).then((result) => {
|
||||||
expect(result).toBeTrue();
|
expect(result).toBeTrue();
|
||||||
expect(provider.create).toHaveBeenCalled();
|
expect(provider.create).toHaveBeenCalled();
|
||||||
openmct.objects.save(mockDomainObject).then((updatedResult) => {
|
|
||||||
|
//Set modified timestamp it detects a change and persists the updated model.
|
||||||
|
mockDomainObject.modified = Date.now();
|
||||||
|
|
||||||
|
return openmct.objects.save(mockDomainObject).then((updatedResult) => {
|
||||||
expect(updatedResult).toBeTrue();
|
expect(updatedResult).toBeTrue();
|
||||||
expect(provider.update).toHaveBeenCalled();
|
expect(provider.update).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('batches requests', () => {
|
||||||
|
let mockPromise;
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPromise = Promise.resolve({
|
||||||
|
json: () => {
|
||||||
|
return {
|
||||||
|
total_rows: 0,
|
||||||
|
rows: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fetch.and.returnValue(mockPromise);
|
||||||
|
});
|
||||||
|
it('for multiple simultaneous gets', () => {
|
||||||
|
const objectIds = [
|
||||||
|
{
|
||||||
|
namespace: '',
|
||||||
|
key: 'object-1'
|
||||||
|
}, {
|
||||||
|
namespace: '',
|
||||||
|
key: 'object-2'
|
||||||
|
}, {
|
||||||
|
namespace: '',
|
||||||
|
key: 'object-3'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
it('updates queued objects', () => {
|
const getAllObjects = Promise.all(
|
||||||
let couchProvider = new CouchObjectProvider(openmct, options, '');
|
objectIds.map((identifier) =>
|
||||||
let intermediateResponse = couchProvider.getIntermediateResponse();
|
openmct.objects.get(identifier)
|
||||||
spyOn(couchProvider, 'updateQueued');
|
));
|
||||||
couchProvider.enqueueObject(mockDomainObject.identifier.key, mockDomainObject, intermediateResponse);
|
|
||||||
couchProvider.objectQueue[mockDomainObject.identifier.key].updateRevision(1);
|
|
||||||
couchProvider.update(mockDomainObject);
|
|
||||||
expect(couchProvider.objectQueue[mockDomainObject.identifier.key].hasNext()).toBe(2);
|
|
||||||
couchProvider.checkResponse({
|
|
||||||
ok: true,
|
|
||||||
rev: 2,
|
|
||||||
id: mockDomainObject.identifier.key
|
|
||||||
}, intermediateResponse);
|
|
||||||
|
|
||||||
expect(couchProvider.updateQueued).toHaveBeenCalledTimes(2);
|
return getAllObjects.then(() => {
|
||||||
|
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||||
|
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestUrl.includes('_all_docs')).toBeTrue();
|
||||||
|
expect(requestMethod).toEqual('POST');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('but not for single gets', () => {
|
||||||
|
const objectId = {
|
||||||
|
namespace: '',
|
||||||
|
key: 'object-1'
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObject = openmct.objects.get(objectId);
|
||||||
|
|
||||||
|
return getObject.then(() => {
|
||||||
|
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||||
|
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
|
||||||
|
expect(requestMethod).toEqual('GET');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('implements server-side search', () => {
|
||||||
|
let mockPromise;
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPromise = Promise.resolve({
|
||||||
|
body: {
|
||||||
|
getReader() {
|
||||||
|
return {
|
||||||
|
read() {
|
||||||
|
return Promise.resolve({
|
||||||
|
done: true,
|
||||||
|
value: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fetch.and.returnValue(mockPromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("using Couch's 'find' endpoint", () => {
|
||||||
|
return Promise.all(openmct.objects.search('test')).then(() => {
|
||||||
|
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalled();
|
||||||
|
expect(requestUrl.endsWith('_find')).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("and supports search by object name", () => {
|
||||||
|
return Promise.all(openmct.objects.search('test')).then(() => {
|
||||||
|
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
||||||
|
|
||||||
|
expect(requestPayload).toBeDefined();
|
||||||
|
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -710,25 +710,25 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async aggregateSearchResults(results, abortSignal) {
|
aggregateSearchResults(results, abortSignal) {
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (!abortSignal.aborted) {
|
if (!abortSignal.aborted) {
|
||||||
const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
|
this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
|
||||||
|
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
||||||
|
objectPath.shift();
|
||||||
|
|
||||||
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
// if root, remove, we're not using in object path for tree
|
||||||
objectPath.shift();
|
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||||
|
if (lastObject && lastObject.type === 'root') {
|
||||||
|
objectPath.pop();
|
||||||
|
}
|
||||||
|
|
||||||
// if root, remove, we're not using in object path for tree
|
// we reverse the objectPath in the tree, so have to do it here first,
|
||||||
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
// since this one is already in the correct direction
|
||||||
if (lastObject && lastObject.type === 'root') {
|
let resultObject = this.buildTreeItem(result, objectPath.reverse());
|
||||||
objectPath.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// we reverse the objectPath in the tree, so have to do it here first,
|
this.searchResultItems.push(resultObject);
|
||||||
// since this one is already in the correct direction
|
});
|
||||||
let resultObject = this.buildTreeItem(result, objectPath.reverse());
|
|
||||||
|
|
||||||
this.searchResultItems.push(resultObject);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user