/*
* 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.
*/
'use strict';
const DataService = require('composer-runtime').DataService;
const Logger = require('composer-common').Logger;
const pouchCollate = require('pouchdb-collate');
const PouchDB = require('pouchdb-core');
const PouchDBDataCollection = require('./pouchdbdatacollection');
const PouchDBUtils = require('./pouchdbutils');
const LOG = Logger.getLog('PouchDBDataService');
// Install the PouchDB plugins. The order of the adapters is important!
PouchDB.plugin(require('pouchdb-find'));
// This is the object type used to form composite keys for the collection of collections.
const collectionObjectType = '$syscollections';
/**
* Base class representing the data service provided by a {@link Container}.
* @protected
*/
class PouchDBDataService extends DataService {
/**
* Register the specified PouchDB plugin with PouchDB.
* @param {*} plugin The PouchDB plugin to register.
*/
static registerPouchDBPlugin (plugin) {
// No logging here as this is called during static initialization
// at startup, and we don't want to try and load the logger yet.
PouchDB.plugin(plugin);
}
/**
* Create a new instance of PouchDB.
* @param {string} name The name of the PouchDB database.
* @param {Object} [options] Optional options for PouchDB.
* @return {PouchDB} The new instance of PouchDB.
*/
static createPouchDB (name, options) {
const method = 'createPouchDB';
LOG.entry(method, name, options);
let result = new PouchDB(name, options);
LOG.exit(method, result);
return result;
}
/**
* Constructor.
* @param {string} [uuid] The UUID of the container.
* @param {boolean} [autocommit] Should this data service auto commit?
* @param {Object} [options] Optional options for PouchDB.
* @param {Object} [additionalConnectorOptions] Additional connector specific options for this transaction.
*/
constructor (uuid, autocommit, options, additionalConnectorOptions = {}) {
super();
const method = 'constructor';
LOG.entry(method, uuid, autocommit, options, additionalConnectorOptions);
this.uuid = uuid;
this.db = PouchDBDataService.createPouchDB('Composer', options);
this.autocommit = !!autocommit;
this.pendingActions = [];
this.additionalConnectorOptions = additionalConnectorOptions;
LOG.exit(method);
}
/**
* Destroy the database.
* @return {Promise} A promise that will be resolved when destroyed, or
* rejected with an error.
*/
destroy () {
const method = 'destroy';
LOG.entry(method);
return this.db.destroy()
.then(() => {
LOG.exit(method);
});
}
/**
* Create a collection with the specified ID.
* @param {string} id The ID of the collection.
* @param {force} [force] force creation, don't check for existence first.
* @return {Promise} A promise that will be resolved with a {@link DataCollection}
* when complete, or rejected with an error.
*/
createCollection (id, force) {
const method = 'createCollection';
LOG.entry(method, id, force);
let compositeKey = [collectionObjectType];
if (this.uuid) {
compositeKey.unshift(this.uuid);
}
compositeKey.push(id);
const key = pouchCollate.toIndexableString(compositeKey);
return PouchDBUtils.getDocument(this.db, key)
.then((doc) => {
if (doc && !force) {
throw new Error(`Failed to add collection with ID '${id}' as the collection already exists`);
}
return this.handleAction(() => {
return PouchDBUtils.putDocument(this.db, key, {});
});
})
.then(() => {
let result = new PouchDBDataCollection(this, this.db, id, this.uuid);
LOG.exit(method, result);
return result;
});
}
/**
* Delete a collection with the specified ID.
* @param {string} id The ID of the collection.
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
deleteCollection (id) {
const method = 'deleteCollection';
LOG.entry(method, id);
let compositeKey = [collectionObjectType];
if (this.uuid) {
compositeKey.unshift(this.uuid);
}
compositeKey.push(id);
const key = pouchCollate.toIndexableString(compositeKey);
return PouchDBUtils.getDocument(this.db, key)
.then((doc) => {
if (!doc) {
throw new Error(`Collection with ID '${id}' does not exist`);
}
return this.handleAction(() => {
return this.clearCollection(id)
.then(() => {
return PouchDBUtils.removeDocument(this.db, key);
});
});
})
.then(() => {
LOG.exit(method);
});
}
/**
* Get the collection with the specified ID.
* @param {string} id The ID of the collection.
* @param {Boolean} bypass bypass existence check
* @return {Promise} A promise that will be resolved with a {@link DataCollection}
* when complete, or rejected with an error.
*/
async getCollection(id, bypass) {
const method = 'getCollection';
LOG.entry(method, id);
if (bypass) {
let result = new PouchDBDataCollection(this, this.db, id, this.uuid);
LOG.exit(method, result);
return result;
} else {
let compositeKey = [collectionObjectType];
if(this.uuid) {
compositeKey.unshift(this.uuid);
}
compositeKey.push(id);
const key = pouchCollate.toIndexableString(compositeKey);
let doc = await PouchDBUtils.getDocument(this.db, key);
if (!doc) {
throw new Error(`Collection with ID '${id}' does not exist`);
}
let result = new PouchDBDataCollection(this, this.db, id, this.uuid);
LOG.exit(method, result);
return result;
}
}
/**
* Remove all the data
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
removeAllData () {
const method = 'removeAllData';
LOG.entry(method);
let compositeKey = [];
if (this.uuid) {
compositeKey.unshift(this.uuid);
}
const startKey = pouchCollate.toIndexableString(compositeKey);
const endCompositeKey = compositeKey;
endCompositeKey.push('\uffff');
const endKey = pouchCollate.toIndexableString(endCompositeKey);
return this.db.allDocs({include_docs : true, startkey : startKey, endkey : endKey, inclusive_end : false})
.then((response) => {
const docs = response.rows.map((row) => {
return {
_id : row.id,
_rev : row.value.rev,
_deleted : true
};
});
return this.db.bulkDocs(docs);
})
.then(() => {
LOG.exit(method);
});
}
/**
* Determine whether the collection with the specified ID exists.
* @param {string} id The ID of the collection.
* @return {Promise} A promise that will be resolved with a boolean
* indicating whether the collection exists.
*/
existsCollection (id) {
const method = 'existsCollection';
LOG.entry(method, id);
let compositeKey = [collectionObjectType];
if (this.uuid) {
compositeKey.unshift(this.uuid);
}
compositeKey.push(id);
const key = pouchCollate.toIndexableString(compositeKey);
return PouchDBUtils.getDocument(this.db, key)
.then((doc) => {
LOG.exit(method, !!doc);
return !!doc;
});
}
/**
* Execute a query across all objects stored in all collections, using a query
* string that is dependent on the current Blockchain platform.
* @param {string} queryString The query string for the current Blockchain platform.
* @return {Promise} A promise that will be resolved with an array of objects
* when complete, or rejected with an error.
*/
executeQuery (queryString) {
const method = 'executeQuery';
LOG.entry(method, queryString);
const query = JSON.parse(queryString);
// PouchDB doesn't deal with $class in the same way that CouchDB does, so
// we need to adapt the selector slightly.
['$class', '$registryType', '$registryId'].forEach((prop) => {
if (query.selector[`\\${prop}`]) {
query.selector[prop] = query.selector[`\\${prop}`];
delete query.selector[`\\${prop}`];
}
});
if (this.uuid) {
query.selector.$networkId = this.uuid;
}
return this.db.find(query)
.then((response) => {
const docs = response.docs.map((doc) => {
delete doc._id;
delete doc._rev;
return doc;
});
LOG.exit(method, docs);
return docs;
});
}
/**
* Remove all objects from the specified collection.
* @param {string} id The ID of the collection.
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
clearCollection (id) {
const method = 'clearCollection';
LOG.entry(method, id);
let compositeKey = [id];
if (this.uuid) {
compositeKey.unshift(this.uuid);
}
const startKey = pouchCollate.toIndexableString(compositeKey);
const endCompositeKey = compositeKey;
endCompositeKey.push('\uffff');
const endKey = pouchCollate.toIndexableString(endCompositeKey);
return this.db.allDocs({startkey : startKey, endkey : endKey, inclusive_end : false})
.then((response) => {
const docs = response.rows.map((row) => {
return {
_id : row.id,
_rev : row.value.rev,
_deleted : true
};
});
return this.db.bulkDocs(docs);
})
.then(() => {
LOG.exit(method);
});
}
/**
* Handle an action against this data service. If auto commit is enabled, then
* the action will be instantly executed. Otherwise it will be queued until the
* transaction is prepared.
* @param {Function} actionFunction The function implementing the acyion.
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
handleAction (actionFunction) {
const method = 'handleAction';
LOG.entry(method, actionFunction);
return Promise.resolve()
.then(() => {
if (this.autocommit) {
LOG.debug(method, 'Autocommit enabled, executing action');
LOG.exit(method);
return actionFunction();
} else {
LOG.debug(method, 'Autocommit disabled, queueing action');
this.pendingActions.push(actionFunction);
LOG.exit(method);
}
});
}
/**
* Called at the start of a transaction.
* @param {boolean} readOnly Is the transaction read-only?
*/
async transactionStart (readOnly) {
const method = 'transactionStart';
LOG.entry(method, readOnly);
await super.transactionStart(readOnly);
this.pendingActions = [];
LOG.exit(method);
}
/**
* Called when a transaction is preparing to commit.
*/
async transactionPrepare () {
const method = 'transactionPrepare';
LOG.entry(method);
await super.transactionPrepare();
if (this.additionalConnectorOptions.commit === false) {
LOG.debug('commit specified as false');
LOG.exit(method);
return;
}
for (const pendingAction of this.pendingActions) {
await pendingAction();
}
LOG.exit(method);
}
}
module.exports = PouchDBDataService;