UNPKG

@corbinu/couchbase

Version:

The unofficial Couchbase Node.js Client Library.

511 lines (449 loc) 12.2 kB
'use strict'; const PromiseHelper = require('./promisehelper'); const HttpExecutor = require('./httpexecutor'); const CompoundTimeout = require('./compoundtimeout'); const errors = require('./errors'); /** * QueryIndex represents a single query index. * * @category Management */ class QueryIndex { constructor() { /** * @type {string} */ this.name = ''; /** * @type {boolean} */ this.isPrimary = false; /** * @type {string} */ this.type = ''; /** * @type {string} */ this.state = ''; /** * @type {string} */ this.keyspace = ''; /** * @type {string[]} */ this.indexKey = []; /** * @type {string?} */ this.condition = null; /** * @type {string?} */ this.partition = ''; } _fromData(data) { this.name = data.name; this.isPrimary = data.is_primary; this.type = data.using; this.state = data.state; this.keyspace = data.keyspace_id; this.indexKey = data.index_key; this.condition = data.condition; this.partition = data.partition; return this; } } /** * QueryIndexManager provides an interface for managing the * query indexes on the cluster. * * @category Management */ class QueryIndexManager { /** * @hideconstructor */ constructor(cluster) { this._cluster = cluster; } get _http() { return new HttpExecutor(this._cluster._getClusterConn()); } async _createIndex(bucketName, options, callback) { return PromiseHelper.wrapAsync(async () => { var qs = ''; if (!options.fields) { qs += 'CREATE PRIMARY INDEX'; } else { qs += 'CREATE INDEX'; } if (options.name) { qs += ' `' + options.name + '`'; } qs += ' ON `' + bucketName + '`'; if (options.fields && options.fields.length > 0) { qs += '('; for (var i = 0; i < options.fields.length; ++i) { if (i > 0) { qs += ', '; } qs += '`' + options.fields[i] + '`'; } qs += ')'; } var withOpts = {}; if (options.deferred) { withOpts.defer_build = true; } if (options.numReplicas) { withOpts.num_replica = options.numReplicas; } if (Object.keys(withOpts).length > 0) { qs += ' WITH ' + JSON.stringify(withOpts); } try { await this._cluster.query(qs, { timeout: options.timeout, }); } catch (err) { if (options.ignoreIfExists && err instanceof errors.IndexExistsError) { // swallow the error if the user wants us to } else { throw err; } } return true; }, callback); } /** * @typedef CreateQueryIndexCallback * @type {function} * @param {Error} err * @param {boolean} res */ /** * * @param {string} bucketName * @param {string} indexName * @param {string[]} fields * @param {Object} [options] * @param {boolean} [options.ignoreIfExists] * @param {boolean} [options.deferred] * @param {number} [options.timeout] * @param {CreateQueryIndexCallback} [callback] * * @throws {IndexExistsError} * @throws {CouchbaseError} * @returns {Promise<boolean>} */ async createIndex(bucketName, indexName, fields, options, callback) { if (options instanceof Function) { callback = arguments[2]; options = undefined; } if (!options) { options = {}; } return PromiseHelper.wrapAsync(async () => { return this._createIndex(bucketName, { name: indexName, fields: fields, ignoreIfExists: options.ignoreIfExists, deferred: options.deferred, timeout: options.timeout, }); }, callback); } /** * @typedef CreatePrimaryIndexCallback * @type {function} * @param {Error} err * @param {boolean} res */ /** * * @param {string} bucketName * @param {Object} [options] * @param {boolean} [options.ignoreIfExists] * @param {boolean} [options.deferred] * @param {number} [options.timeout] * @param {CreatePrimaryIndexCallback} [callback] * * @throws {IndexExistsError} * @throws {CouchbaseError} * @returns {Promise<boolean>} */ async createPrimaryIndex(bucketName, options, callback) { if (options instanceof Function) { callback = arguments[0]; options = undefined; } if (!options) { options = {}; } return PromiseHelper.wrapAsync(async () => { return this._createIndex(bucketName, { name: options.name, ignoreIfExists: options.ignoreIfExists, deferred: options.deferred, timeout: options.timeout, }); }, callback); } async _dropIndex(bucketName, options, callback) { return PromiseHelper.wrapAsync(async () => { var qs = ''; if (!options.name) { qs += 'DROP PRIMARY INDEX `' + bucketName + '`'; } else { qs += 'DROP INDEX `' + bucketName + '`.`' + options.name + '`'; } try { await this._cluster.query(qs, { timeout: options.timeout, }); } catch (err) { if ( options.ignoreIfNotExists && err instanceof errors.QueryIndexNotFoundError ) { // swallow the error if the user wants us to } else { throw err; } } return true; }, callback); } /** * @typedef DropQueryIndexCallback * @type {function} * @param {Error} err * @param {boolean} res */ /** * * @param {string} bucketName * @param {string} indexName * @param {Object} [options] * @param {boolean} [options.ignoreIfNotExists] * @param {number} [options.timeout] * @param {DropQueryIndexCallback} [callback] * * @throws {QueryIndexNotFoundError} * @throws {CouchbaseError} * @returns {Promise<boolean>} */ async dropIndex(bucketName, indexName, options, callback) { if (options instanceof Function) { callback = arguments[2]; options = undefined; } if (!options) { options = {}; } return PromiseHelper.wrapAsync(async () => { return this._dropIndex(bucketName, { name: indexName, ignoreIfNotExists: options.ignoreIfNotExists, timeout: options.timeout, }); }, callback); } /** * @typedef DropPrimaryIndexCallback * @type {function} * @param {Error} err * @param {boolean} res */ /** * * @param {string} bucketName * @param {Object} [options] * @param {boolean} [options.ignoreIfNotExists] * @param {number} [options.timeout] * @param {DropPrimaryIndexCallback} [callback] * * @throws {QueryIndexNotFoundError} * @throws {CouchbaseError} * @returns {Promise<boolean>} */ async dropPrimaryIndex(bucketName, options, callback) { if (options instanceof Function) { callback = arguments[0]; options = undefined; } if (!options) { options = {}; } return PromiseHelper.wrapAsync(async () => { return this._dropIndex(bucketName, { name: options.name, ignoreIfNotExists: options.ignoreIfNotExists, timeout: options.timeout, }); }, callback); } /** * @typedef GetAllQueryIndexesCallback * @type {function} * @param {Error} err * @param {QueryIndex[]} res */ /** * * @param {string} bucketName * @param {Object} [options] * @param {number} [options.timeout] * @param {GetAllQueryIndexesCallback} [callback] * * @throws {CouchbaseError} * @returns {Promise<QueryIndex[]>} */ async getAllIndexes(bucketName, options, callback) { if (options instanceof Function) { callback = arguments[1]; options = undefined; } if (!options) { options = {}; } return PromiseHelper.wrapAsync(async () => { var qs = ''; qs += 'SELECT idx.* FROM system:indexes AS idx'; qs += ' WHERE keyspace_id="' + bucketName + '"'; qs += ' AND `using`="gsi" ORDER BY is_primary DESC, name ASC'; var res = await this._cluster.query(qs, { timeout: options.timeout, }); var indexes = []; res.rows.forEach((row) => { indexes.push(new QueryIndex()._fromData(row)); }); return indexes; }, callback); } /** * @typedef BuildDeferredIndexesCallback * @type {function} * @param {Error} err * @param {string[]} res */ /** * * @param {string} bucketName * @param {Object} [options] * @param {number} [options.timeout] * @param {BuildDeferredIndexesCallback} [callback] * * @throws {CouchbaseError} * @returns {Promise<string[]>} */ async buildDeferredIndexes(bucketName, options, callback) { if (options instanceof Function) { callback = arguments[1]; options = undefined; } if (!options) { options = {}; } var timer = new CompoundTimeout(options.timeout); return PromiseHelper.wrapAsync(async () => { var indices = await this.getAllIndexes(bucketName, { timeout: timer.left(), }); var deferredList = []; for (var i = 0; i < indices.length; ++i) { var index = indices[i]; if (index.state === 'deferred' || index.state === 'pending') { deferredList.push(index.name); } } // If there are no deferred indexes, we have nothing to do. if (deferredList.length === 0) { return []; } var qs = ''; qs += 'BUILD INDEX ON `' + bucketName + '` '; qs += '('; for (var j = 0; j < deferredList.length; ++j) { if (j > 0) { qs += ', '; } qs += '`' + deferredList[j] + '`'; } qs += ')'; // Run our deferred build query await this._cluster.query(qs, { timeout: timer.left(), }); // Return the list of indices that we built return deferredList; }, callback); } /** * @typedef WatchIndexesCallback * @type {function} * @param {Error} err * @param {boolean} res */ /** * * @param {string} bucketName * @param {string[]} indexNames * @param {number} duration * @param {Object} [options] * @param {number} [options.watchPrimary] * @param {WatchIndexesCallback} [callback] * * @throws {CouchbaseError} * @returns {Promise<boolean>} */ async watchIndexes(bucketName, indexNames, duration, options, callback) { if (options instanceof Function) { callback = arguments[1]; options = undefined; } if (!options) { options = {}; } var timer = new CompoundTimeout(duration); return PromiseHelper.wrapAsync(async () => { if (options.watchPrimary) { indexNames = [...indexNames, '#primary']; } var curInterval = 50; for (;;) { // Get all the indexes that are currently registered var foundIdxs = await this.getAllIndexes(bucketName, { timeout: timer.left(), }); var onlineIdxs = foundIdxs.filter((idx) => idx.state === 'online'); var onlineIdxNames = onlineIdxs.map((idx) => idx.name); // Check if all the indexes we want are online var allOnline = true; indexNames.forEach((indexName) => { allOnline &= onlineIdxNames.indexOf(indexName) !== -1; }); // If all the indexes are online, we've succeeded if (allOnline) { break; } // Add 500 to our interval to a max of 1000 curInterval = Math.min(curInterval, curInterval + 500); // Make sure we don't go past our user-specified duration curInterval = Math.min(curInterval, timer.left()); if (curInterval <= 0) { throw new errors.CouchbaseError( 'Failed to find all indexes online within the alloted time.' ); } // Wait until curInterval expires await new Promise((resolve) => setTimeout(resolve, curInterval)); } }, callback); } } module.exports = QueryIndexManager;