markin-couchbase
Version:
Markin Fork of Couchbase Node.js Client Library.
1,732 lines (1,624 loc) • 49.1 kB
JavaScript
'use strict';
var util = require('util');
var fs = require('fs');
var path = require('path');
var JsonParser = require('jsonparse');
var qs = require('querystring');
var request = require('request');
var dns = require('dns');
var events = require('events');
var http = require('http');
var url = require('url');
var binding = require('./binding');
var connStr = require('./connstr');
var ViewQuery = require('./viewquery');
var SpatialQuery = require('./spatialquery');
var N1qlQuery = require('./n1qlquery');
var BucketManager = require('./bucketmgr');
var CONST = binding.Constants;
var CBpp = binding.CouchbaseImpl;
/**
* The *CAS* value is a special object that indicates the current state
* of the item on the server. Each time an object is mutated on the server, the
* value is changed. <i>CAS</i> objects can be used in conjunction with
* mutation operations to ensure that the value on the server matches the local
* value retrieved by the client. This is useful when doing document updates
* on the server as you can ensure no changes were applied by other clients
* while you were in the process of mutating the document locally.
*
* In the Node.js SDK, the CAS is represented as an opaque value. As such,y
* ou cannot generate CAS objects, but should rather use the values returned
* from a {@link Bucket.OpCallback}.
*
* @typedef {Object} Bucket.CAS
*/
/**
* @class CouchbaseError
* @classdesc
* The virtual class thrown for all Couchnode errors.
* @private
* @extends Error
*/
/**
* The error code for this error.
* @var {errors} CouchbaseError#code
*/
/**
* Single-Key callbacks.
*
* This callback is passed to all of the single key functions.
*
* It returns a result objcet containing a combination of a CAS and a value,
* depending on which operation was invoked.
*
* @typedef {function} Bucket.OpCallback
*
* @param {undefined|Error} error
* The error for the operation. This can either be an Error object
* or a value which evaluates to false (null, undefined, 0 or false).
* @param {Object} result
* The result of the operation that was executed. This usually contains
* at least a <i>cas</i> property, and on some operations will contain a
* <i>value</i> property as well.
*/
/**
* Multi-Get Callback.
*
* This callback is used to return results from a getMulti operation.
*
* @typedef {function} Bucket.MultiGetCallback
*
* @param {undefined|number} error
* The number of keys that failed to be retrieved. The precise errors
* are available by checking the error property of the individual documents.
* @param {Array.<Object>} results
* This is a map of keys to results. The result for each key will optionally
* contain an error if one occured, or if no error occured will contain
* the CAS and value of the document.
*/
/**
* This is used as a callback from executed queries. It is a shortcut method
* that automatically subscribes to the rows and error events of the
* {@link Bucket.ViewQueryResponse}.
*
* @typedef {function} Bucket.QueryCallback
*
* @param {undefined|Error} error
* The error for the operation. This can either be an Error object
* or a falsy value.
* @param {Array.<Object>} rows
* The rows returned from the query
* @param {Bucket.ViewQueryResponse.Meta} meta
* The metadata returned by the query.
*/
/**
* @class Bucket.TranscoderDoc
* @classdesc
* A class used in relation to transcoders
*
* @property {Buffer} value
* @property {number} flags
*
* @since 2.0.0
* @uncommitted
*/
/**
* Transcoder Encoding Function.
*
* This function will receive a value when a storage operation is invoked
* that needs to encode user-provided data for storage into Couchbase. It
* expects to be returned a `Buffer` object to store along with an integer
* representing any flag metadata relating to how to decode the key later
* using the matching {@link DecoderFunction}.
*
* @typedef {function} Bucket.EncoderFunction
*
* @param {*} value The value needing encoding.
* @returns {!Bucket.TranscoderDoc} The data to store to Couchbase.
*/
/**
* Transcoder Decoding Function.
*
* This function will receive an object containing a `Buffer` value and an
* integer value representing any flags metadata whenever a retrieval
* operation is executed. It is expected that this function will return a
* value representing the original value stored and encoded with its
* matching {@link EncoderFunction}.
*
* @typedef {function} Bucket.DecoderFunction
*
* @param {!Bucket.TranscoderDoc} doc The data from Couchbase to decode.
* @returns {*} The resulting value.
*/
/**
* @class
* The Bucket class represents a connection to a Couchbase bucket. Never
* instantiate this class directly. Instead use the {@link Cluster#openBucket}
* method instead.
*
* @private
*
* @since 2.0.0
* @committed
*/
function Bucket(options) {
// We normalize both for consistency as well as to
// create a duplicate object to use
options.dsnObj = connStr.normalize(options.dsnObj);
var bucketDsn = connStr.stringify(options.dsnObj);
var bucketUser = options.username;
var bucketPass = options.password;
this._name = options.dsnObj.bucket;
this._username = options.username;
this._password = options.password;
this._cb = new CBpp(bucketDsn, bucketUser, bucketPass);
this.connected = null;
this._cb.setConnectCallback(function(err) {
if (err) {
this.connected = false;
var errObj = new Error('failed to connect to bucket');
errObj.code = err;
return this.emit('error', errObj);
}
this.connected = true;
this.emit('connect');
}.bind(this));
this.waitQueue = [];
this.on('connect', function() {
for (var i = 0; i < this.waitQueue.length; ++i) {
var itm = this.waitQueue[i];
this._invoke(itm[0], itm[1]);
}
this.waitQueue = [];
});
this.on('error', function(err) {
for (var i = 0; i < this.waitQueue.length; ++i) {
var itm = this.waitQueue[i];
itm[1][itm[1].length-1](err, null);
}
this.waitQueue = [];
});
this.httpAgent = new http.Agent();
this.httpAgent.maxSockets = 250;
/* istanbul ignore else */
if (options.dsnObj.hosts.length !== 1 ||
options.dsnObj.hosts[0][1] ||
(options.dsnObj.scheme !== 'couchbase' &&
options.dsnObj.scheme !== 'couchbases')) {
// We perform the connect on the next tick to ensure
// consistent behaviour between SRV and non-SRV.
process.nextTick(function() {
this._cb.connect();
}.bind(this));
} else {
var srvHost = options.dsnObj.hosts[0][0];
var srvPrefix = '_' + options.dsnObj.scheme;
dns.resolveSrv(srvPrefix + '.' + srvHost, function(err, addrs) {
if (!err) {
options.dsnObj.hosts = [];
for (var i = 0; i < addrs.length; ++i) {
options.dsnObj.hosts.push([addrs[i].name, addrs[i].port]);
}
var srvDsn = connStr.stringify(options.dsnObj);
this._cb.control(CONST.CNTL_REINIT_DSN, CONST.CNTL_SET, srvDsn);
}
this._cb.connect();
}.bind(this));
}
}
util.inherits(Bucket, events.EventEmitter);
/**
* Connected Event.
* Invoked once the connection has been established successfully.
*
* @event Bucket#connect
*
* @since 2.0.0
* @committed
*/
/**
* Error Event.
* Invoked if the connection encounters any errors without having an
* operation context available to handle the error.
*
* @event Bucket#error
* @param {Error} err
* The error that occured while attempting to connect to the cluster.
*
* @since 2.0.0
* @committed
*/
/**
* Enables N1QL support on the client. A cbq-server URI must be passed. This
* method will be deprecated in the future in favor of automatic configuration
* through the connected cluster.
*
* @param {string|string[]} hosts
* An array of host/port combinations which are N1QL servers attached to
* this cluster.
*
* @example
* bucket.enableN1ql(['http://1.1.1.1:8093/','http://1.1.1.2:8093']);
*
* @since 2.0.0
* @volatile
*/
/* istanbul ignore next */
Bucket.prototype.enableN1ql = function(hosts) {
if (Array.isArray(hosts)) {
this.queryhosts = hosts;
} else {
this.queryhosts = [hosts];
}
};
/**
* Returns an instance of a {@link BuckerManager} for performing management
* operations against a bucket.
*
* @returns {BucketManager}
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.manager = function() {
return new BucketManager(this);
};
/**
* Shuts down this connection.
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.disconnect = function() {
if (this.connected !== false) {
this.connected = false;
this._cb.shutdown();
}
};
/**
* Configures a custom set of transcoder functions for encoding and decoding
* values that are being stored or retreived from the server.
*
* @param {EncoderFunction} encoder The function for encoding.
* @param {DecoderFunction} decoder The function for decoding.
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.setTranscoder = function(encoder, decoder) {
this._cb.setTranscoder(encoder, decoder);
};
/**
* Picks a random CAPI node and builds an http or https request against
* it using the passed path.
*
* @param path
* @param method
* @returns {http.ClientRequest}
*
* @private
* @ignore
*/
Bucket.prototype._capiRequest = function(path, method, callback) {
var nodeUri = url.parse(this._cb.getViewNode.call(this._cb));
console.log(this._cb.getViewNode.call(this._cb));
var reqOpts = {
agent: this.httpAgent,
hostname: nodeUri.hostname,
port: nodeUri.port,
path: (nodeUri.path!=='/'?nodeUri.path:'') + '/' + path,
method: method,
headers: { 'Content-Type': 'application/json' }
};
if (this._password) {
reqOpts.auth = this._username + ':' + this._password;
}
callback(null, http.request(reqOpts));
};
/**
* Picks a random management node and builds an http or https request against
* it using the passsed path.
*
* @param path
* @returns {http.ClientRequest}
*
* @private
* @ignore
*/
Bucket.prototype._mgmtRequest = function(path, method, callback) {
var nodeUri = url.parse('http://'+this._cb.getMgmtNode.call(this._cb));
var reqOpts = {
hostname: nodeUri.hostname,
port: nodeUri.port,
path: (nodeUri.path!=='/'?nodeUri.path:'') + '/' + path,
method: method,
headers: { 'Content-Type': 'application/json' }
};
if (this._password) {
reqOpts.auth = this._username + ':' + this._password;
}
callback(null, http.request(reqOpts));
};
/**
* @class Meta
* @classdesc
* The meta-information available from a view query response.
* @private
* @memberof Bucket.ViewQueryResponse
*/
/**
* The total number of rows available in the index of the view
* that was queried.
*
* @var {number} Bucket.ViewQueryResponse.Meta#total_rows
* @since 2.0.0
* @committed
*/
/**
* Emitted whenever a new row is available from a queries result set.
*
* @event Bucket.ViewQueryResponse#row
* @param {Object} row
* @param {Bucket.ViewQueryResponse.Meta} meta
*
* @since 2.0.0
* @committed
*/
/**
* Emitted whenever all rows are available from a queries result set.
*
* @event Bucket.ViewQueryResponse#rows
* @param {Object[]} rows
* @param {Bucket.ViewQueryResponse.Meta} meta
*
* @since 2.0.0
* @committed
*/
/**
* Emitted once a query has completed executing and emitting all rows.
*
* @event Bucket.ViewQueryResponse#end
* @param {Bucket.ViewQueryResponse.Meta} meta
*
* @since 2.0.0
* @committed
*/
/**
* Emitted if an error occurs while executing a query.
*
* @event Bucket.ViewQueryResponse#error
* @param {Error} error
*
* @since 2.0.0
* @committed
*/
/**
* An event emitter allowing you to bind to various query result set
* events.
*
* @constructor
*
* @private
* @memberof Bucket
* @extends events.EventEmitter
*
* @since 2.0.0
* @committed
*/
function ViewQueryResponse() {
}
util.inherits(ViewQueryResponse, events.EventEmitter);
/**
* Executes a view http request.
*
* @param {string} viewtype
* @param {string} ddoc
* @param {string} name
* @param {Object} q
* @param {ViewQueryResponse} emitter
*
* @private
* @ignore
*/
Bucket.prototype._viewReq = function(viewtype, ddoc, name, q, emitter) {
var isSpatial = false;
if (viewtype === '_spatial') {
isSpatial = true;
}
var includeDocs = false;
var opts = {};
for (var i in q) {
if (q.hasOwnProperty(i)) {
if (i === 'include_docs') {
if (q[i] && q[i] === 'true') {
includeDocs = true;
} else {
includeDocs = false;
}
} else {
opts[i] = q[i];
}
}
}
var rows = [];
this._cb.viewQuery(
isSpatial,
ddoc, name,
qs.stringify(opts),
includeDocs,
function(errCode, val) {
if (errCode === -1) {
var row = val;
if (rows) {
if (events.EventEmitter.listenerCount(emitter, 'rows') > 0) {
rows.push(row);
} else {
rows = null;
}
}
emitter.emit('row', row);
} else if (errCode === 0) {
var meta = val;
if (rows) {
emitter.emit('rows', rows, meta);
}
emitter.emit('end', meta);
} else {
var errStr = val;
var jsonError = JSON.parse(errStr);
var errorMessage = 'unknown error : error parsing failed';
if (jsonError) {
errorMessage = jsonError.message;
if (jsonError.error || jsonError.reason) {
var subError = jsonError.error + ': ' + jsonError.reason;
if (!errorMessage) {
errorMessage = subError;
} else {
errorMessage += ' (' + subError + ')';
}
}
}
emitter.emit('error', new Error(errorMessage));
}
});
};
/**
* Performs a view request.
*
* @param {string} viewtype
* @param {string} ddoc
* @param {string} name
* @param {Object} q
* @param {function(err,res,meta)} callback
*
* @private
* @ignore
*/
Bucket.prototype._view = function(viewtype, ddoc, name, q, callback) {
var path = '_design/' + ddoc + '/' + viewtype + '/' +
name + '?' + qs.stringify(q);
var req = new ViewQueryResponse();
this._maybeInvoke(this._viewReq.bind(this),
[viewtype, ddoc, name, q, req, callback]);
if (callback) {
req.on('rows', function(rows, meta) {
callback(null, rows, meta);
});
req.on('error', function(err) {
callback(err, null, null);
});
}
return req;
};
/**
* @class Meta
* @classdesc
* The meta-information available from a view query response.
* @private
* @memberof Bucket.N1qlQueryResponse
*/
/**
* The identifier for this query request.
*
* @var {number} Bucket.N1qlQueryResponse.Meta#requestID
* @since 2.0.8
* @committed
*/
/**
* Emitted whenever a new row is available from a queries result set.
*
* @event Bucket.N1qlQueryResponse#row
* @param {Object} row
* @param {Bucket.N1qlQueryResponse.Meta} meta
*
* @since 2.0.8
* @committed
*/
/**
* Emitted whenever all rows are available from a queries result set.
*
* @event Bucket.N1qlQueryResponse#rows
* @param {Object[]} rows
* @param {Bucket.N1qlQueryResponse.Meta} meta
*
* @since 2.0.8
* @committed
*/
/**
* Emitted once a query has completed executing and emitting all rows.
*
* @event Bucket.N1qlQueryResponse#end
* @param {Bucket.N1qlQueryResponse.Meta} meta
*
* @since 2.0.8
* @committed
*/
/**
* Emitted if an error occurs while executing a query.
*
* @event Bucket.N1qlQueryResponse#error
* @param {Error} error
*
* @since 2.0.8
* @committed
*/
/**
* An event emitter allowing you to bind to various query result set
* events.
*
* @constructor
*
* @private
* @memberof Bucket
* @extends events.EventEmitter
*
* @since 2.0.8
* @committed
*/
function N1qlQueryResponse() {
}
util.inherits(N1qlQueryResponse, events.EventEmitter);
/**
* Executes a N1QL http request.
*
* @param {string} queryStr
* @param {N1qlQueryResponse} emitter
*
* @private
* @ignore
*/
Bucket.prototype._n1qlReq = function(host, q, emitter) {
var rows = [];
this._cb.n1qlQuery(
host, q,
function(errCode, val) {
if (errCode === -1) { // Row
var row = val;
rows.push(row);
emitter.emit('row', row);
} else if (errCode === 0) { // Success
var meta = val;
emitter.emit('rows', rows, meta);
emitter.emit('end', meta);
} else { // Error
var errStr = val;
var jsonError = JSON.parse(errStr);
var err;
if (jsonError.errors.length > 0) {
var firstErr = jsonError.errors[0];
err = new Error(firstErr.msg);
err.code = firstErr.code;
err.otherErrors = [];
for (var i = 1; i < jsonError.errors.length; ++i) {
var nextErr = jsonError.errors[i];
var otherErr = new Error(nextErr.msg);
otherErr.code = nextErr.code;
err.otherErrors.push(otherErr);
}
} else {
err = new Error('An unknown error occured');
}
err.requestID = jsonError.requestID;
emitter.emit('error', err);
}
});
};
/**
* Executes a N1QL query from a N1QL query string.
*
* @param {string} query
* @param {function} callback
* @private
* @ignore
*/
Bucket.prototype._n1ql = function(query, params, callback) {
var host;
if (this.queryhosts) {
var qhosts = this.queryhosts;
host = qhosts[Math.floor(Math.random() * qhosts.length)];
if (host.indexOf(':') === -1) {
host = host + ':8093';
}
}
var req = new N1qlQueryResponse();
this._maybeInvoke(this._n1qlReq.bind(this),
[host, query.toObject(params), req, callback]);
if (callback) {
req.on('rows', function(rows, meta) {
callback(null, rows, meta);
});
req.on('error', function(err) {
callback(err, null, null);
});
}
return req;
};
/**
* Executes a previously prepared query object. This could be a
* {@link ViewQuery} or a {@link N1qlQuery}.
*
* Note: N1qlQuery queries are currently an uncommitted interface and may be
* subject to change in 2.0.0's final release.
*
* @param {ViewQuery|N1qlQuery} query
* The query to execute.
* @param {Object|Array} [params]
* A list or map to do replacements on a N1QL query.
* @param {Bucket.QueryCallback} callback
* @returns {Bucket.ViewQueryResponse|Bucket.N1qlQueryResponse}
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.query = function(query, params, callback) {
if (params instanceof Function) {
callback = arguments[1];
params = undefined;
}
if (query instanceof ViewQuery) {
return this._view(
'_view', query.ddoc, query.name, query.options, callback);
} else if (query instanceof SpatialQuery) {
return this._view(
'_spatial', query.ddoc, query.name, query.options, callback);
} else if (query instanceof N1qlQuery) {
return this._n1ql(
query, params, callback
);
} else {
throw new TypeError(
'First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.');
}
};
/**
* Gets or sets a libcouchbase instance setting.
*
* @private
* @ignore
*/
Bucket.prototype._ctl = function(cc, value) {
if (value !== undefined) {
return this._cb.control.call(this._cb, cc, CONST.CNTL_SET, value);
} else {
return this._cb.control.call(this._cb, cc, CONST.CNTL_GET);
}
};
/**
* Creates an durability failure Error object.
*
* @param {Error} innerError
* The internal error that occured that caused the durability requirements to
* fail to succeed.
* @returns {CouchbaseError}
*
* @private
* @ignore
*/
function _endureError(innerError)
{
var out_error = new binding.Error('Durability requirements failed');
out_error.code = CONST['ErrorCode::DURABILITY_FAILED'];
out_error.innerError = innerError;
return out_error;
}
/**
* Common callback interceptor for durability requirements. This function
* will wrap user-provided callbacks in a handler which will ensure all
* durability requirements are met prior to invoking the user-provided callback.
*
* @private
* @ignore
*/
Bucket.prototype._interceptEndure =
function(key, options, is_delete, callback) {
if (!options.persist_to && !options.replicate_to) {
// leave early if we can
return callback;
}
// Return our interceptor
var _this = this;
return function(err, res) {
if (err) {
callback(err, res);
return;
}
_this._cb.durability.call(_this._cb, key, options.hashkey, res.cas,
options.persist_to, options.replicate_to, is_delete,
function(endure_err) {
if(endure_err) {
callback(_endureError(endure_err), res);
return;
}
callback(err, res);
});
};
};
/**
* Invokes an operation and dispatches a callback error if one occurs.
*
* @param {Function} fn The operation callback to invoke.
* @param {Array.<*>} args An array of arguments to pass to the function.
*
* @private
* @ignore
*/
Bucket.prototype._invoke = function(fn, args) {
try {
fn.apply(this._cb, args);
} catch(e) {
args[args.length-1](e, null);
}
};
/**
* Will either invoke the binding method specified by fn, or alternatively
* push the operation to a queue which is flushed once a connection
* has been established or failed.
*
* @param {Function} fn The binding method to invoke.
* @param {Array} args A list of arguments for the method.
*
* @private
* @ignore
*/
Bucket.prototype._maybeInvoke = function(fn, args) {
if (this.connected === true) {
this._invoke(fn, args);
} else if (this.connected === false) {
throw new Error('cannot perform operations on a shutdown bucket');
} else {
this.waitQueue.push([fn, args]);
}
};
/**
* Deduces if the C++ binding layer will accept the passed value
* as an appropriately typed key.
*
* @param {string|Buffer} key The key
* @returns {boolean}
*
* @private
* @ignore
*/
Bucket.prototype._isValidKey = function(key) {
return typeof key === 'string' || key instanceof Buffer;
};
/**
* Checks that the passed options have a valid hashkey specified.
* Note that hashkey/groupid is not a supported feature of Couchbase Server
* and this client. It should be considered volatile and experimental. Using
* this could lead to an unbalanced cluster, inability to interoperate with the
* data from other languages, not being able to use the Couchbase Server UI to
* look up documents and other possible future upgrade/migration concerns.
*
* @param {Object} options The options objects to check.
*
* @private
* @ignore
*/
Bucket.prototype._checkHashkeyOption = function(options) {
if (options.hashkey !== undefined) {
if (!this._isValidKey(options.hashkey)) {
throw new TypeError('hashkey option needs to be a string or buffer.');
}
}
};
/**
* Checks that the passed options have a valid expiry specified.
*
* @param {Object} options The options objects to check.
*
* @private
* @ignore
*/
Bucket.prototype._checkExpiryOption = function(options) {
if (options.expiry !== undefined) {
if (typeof options.expiry !== 'number' || options.expiry < 0) {
throw new TypeError('expiry option needs to be 0 or a positive integer.');
}
}
};
/**
* Checks that the passed options have a valid cas specified.
*
* @param {Object} options The options objects to check.
*
* @private
* @ignore
*/
Bucket.prototype._checkCasOption = function(options) {
if (options.cas !== undefined) {
if (typeof options.cas !== 'object' && typeof options.cas !== 'string') {
throw new TypeError('cas option needs to be a CAS object or string.');
}
}
};
/**
* Checks that the passed options have a valid persist_to
* and replicate_to specified.
*
* @param {Object} options The options objects to check.
*
* @private
* @ignore
*/
Bucket.prototype._checkDuraOptions = function(options) {
if (options.persist_to !== undefined) {
if (typeof options.persist_to !== 'number' ||
options.persist_to < 0 || options.persist_to > 8) {
throw new TypeError(
'persist_to option needs to be an integer between 0 and 8.');
}
}
if (options.replicate_to !== undefined) {
if (typeof options.replicate_to !== 'number' ||
options.replicate_to < 0 || options.replicate_to > 8) {
throw new TypeError(
'replicate_to option needs to be an integer between 0 and 8.');
}
}
};
/**
* Retrieves a document.
*
* @param {string|Buffer} key
* The target document key.
* @param {Object} [options]
* @param {Bucket.OpCallback} callback
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.get = function(key, options, callback) {
if (options instanceof Function) {
callback = arguments[1];
options = {};
}
if (!this._isValidKey(key)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof options !== 'object') {
throw new TypeError('Second argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Third argument needs to be a callback.');
}
this._checkHashkeyOption(options);
this._maybeInvoke(this._cb.get, [key, options.hashkey, 0, 0, callback]);
};
/**
* Retrieves a list of keys
*
* @param {Array.<Buffer|string>} keys
* The target document keys.
* @param {Bucket.MultiGetCallback} callback
*
* @see Bucket#get
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.getMulti = function(keys, callback) {
if (!Array.isArray(keys) || keys.length === 0) {
throw new TypeError('First argument needs to be an array of length > 0.');
}
if (typeof callback !== 'function') {
throw new TypeError('Second argument needs to be a callback.');
}
var self = this;
var outMap = {};
var resCount = 0;
var errCount = 0;
function getSingle(key) {
self.get(key, function(err, res) {
resCount++;
if (err) {
errCount++;
outMap[key] = { error: err };
} else {
outMap[key] = res;
}
if (resCount === keys.length) {
return callback(errCount, outMap);
}
});
}
for (var i = 0; i < keys.length; ++i) {
getSingle(keys[i]);
}
};
/**
* Retrieves a document and updates the expiry of the item at the same time.
*
* @param {string|Buffer} key
* The target document key.
* @param {number} expiry
* The expiration time to use. If a value of 0 is provided, then the
* current expiration time is cleared and the key is set to
* never expire. Otherwise, the key is updated to expire in the
* time provided (in seconds).
* @param {Object} [options]
* @param {Bucket.OpCallback} callback
*
* @see Bucket#get
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.getAndTouch = function(key, expiry, options, callback) {
if (options instanceof Function) {
callback = arguments[2];
options = {};
}
if (typeof key !== 'string' && !(key instanceof Buffer)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof expiry !== 'number' || expiry < 0) {
throw new TypeError('Second argument needs to be 0 or a positive integer.');
}
if (typeof options !== 'object') {
throw new TypeError('Third argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Fourth argument needs to be a callback.');
}
this._checkHashkeyOption(options);
this._checkDuraOptions(options);
this._maybeInvoke(this._cb.get, [key, options.hashkey, expiry, 0, callback]);
};
/**
* Lock the document on the server and retrieve it. When an document is locked,
* its CAS changes and subsequent operations on the document (without providing
* the current CAS) will fail until the lock is no longer held.
*
* This function behaves identically to {@link Bucket#get} in that it will
* return the value. It differs in that the document is also locked. This
* ensures that attempts by other client instances to access this document
* while the lock is held will fail.
*
* Once locked, a document can be unlocked either by explicitly calling
* {@link Bucket#unlock} or by performing a storage operation
* (e.g. {@link Bucket#set}, {@link Bucket#replace}, {@link Bucket::append})
* with the current CAS value. Note that any other lock operations on this
* key will fail while a document is locked.
*
* @param {string|Buffer} key
* The target document key.
* @param {Object} [options]
* @param {number} [options.lockTime=15]
* The duration of time the lock should be held for. Note that the maximum
* duration for a lock is 30 seconds, and if a higher value is specified,
* it will be rounded to this number.
* @param {Bucket.OpCallback} callback
*
* @see Bucket#get
* @see Bucekt#unlock
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.getAndLock = function(key, options, callback) {
if (options instanceof Function) {
callback = arguments[1];
options = {};
}
if (!this._isValidKey(key)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof options !== 'object') {
throw new TypeError('Second argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Third argument needs to be a callback.');
}
if (options.lockTime !== undefined) {
if (typeof options.lockTime !== 'number' || options.lockTime < 1) {
throw new TypeError('lockTime option needs to be a positive integer.');
}
}
this._checkHashkeyOption(options);
this._maybeInvoke(this._cb.get,
[key, options.hashkey, options.lockTime, 1, callback]);
};
/**
* Get a document from a replica server in your cluster.
*
* @param {string|Buffer} key
* The target document key.
* @param {Object} [options]
* @param {number} [options.index=undefined]
* The index for which replica you wish to retrieve this value from, or
* if undefined, use the value from the first server that replies.
* @param {Bucket.OpCallback} callback
*
* @see Bucket#get
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.getReplica = function(key, options, callback) {
if (options instanceof Function) {
callback = arguments[1];
options = {};
}
if (typeof key !== 'string' && !(key instanceof Buffer)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof options !== 'object') {
throw new TypeError('Second argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Third argument needs to be a callback.');
}
if (options.hashkey !== undefined) {
if (!this._isValidKey(options.hashkey)) {
throw new TypeError('hashkey option needs to be a string or buffer.');
}
}
this._checkHashkeyOption(options);
this._maybeInvoke(this._cb.getReplica,
[key, options.hashkey, options.index, callback]);
};
/**
* Update the document expiration time.
*
* @param {string|Buffer} key
* The target document key.
* @param {number} expiry
* The expiration time to use. If a value of 0 is provided, then the
* current expiration time is cleared and the key is set to
* never expire. Otherwise, the key is updated to expire in the
* time provided (in seconds). Values larger than 30*24*60*60 seconds
* (30 days) are interpreted as absolute times (from the epoch).
* @param {Object} [options]
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback.
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.touch = function(key, expiry, options, callback) {
if (options instanceof Function) {
callback = arguments[2];
options = {};
}
if (!this._isValidKey(key)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof expiry !== 'number' || expiry < 0) {
throw new TypeError('Second argument needs to be 0 or a positive integer..');
}
if (typeof options !== 'object') {
throw new TypeError('Third argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Fourth argument needs to be a callback.');
}
this._checkHashkeyOption(options);
this._checkCasOption(options);
this._checkDuraOptions(options);
this._maybeInvoke(this._cb.get, [key, options.hashkey, expiry, 0, callback]);
};
/**
* Unlock a previously locked document on the server. See the
* {@link Bucket#lock} method for more details on locking.
*
* @param {string|Buffer} key
* The target document key.
* @param {Bucket.CAS} cas
* The CAS value returned when the key was locked. This operation will fail
* if the CAS value provided does not match that which was the result of the
* original lock operation.
* @param {Object} [options]
* @param {Bucket.OpCallback} callback
*
* @see Bucket#getAndLock
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.unlock = function(key, cas, options, callback) {
if (options instanceof Function) {
callback = arguments[2];
options = {};
}
if (typeof key !== 'string' && !(key instanceof Buffer)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof cas !== 'object') {
throw new TypeError('Second argument needs to be a CAS object.');
}
if (typeof options !== 'object') {
throw new TypeError('Third argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Fourth argument needs to be a callback.');
}
this._checkHashkeyOption(options);
this._maybeInvoke(this._cb.unlock, [key, options.hashkey, cas, callback]);
};
/**
* Deletes a document on the server.
*
* @param {string|Buffer} key
* The target document key.
* @param {Object} [options]
* @param {Bucket.CAS} [options.cas=undefined]
* The CAS value to check. If the item on the server contains a different
* CAS value, the operation will fail. Note that if this option is undefined,
* no comparison will be performed.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.remove = function(key, options, callback) {
if (options instanceof Function) {
callback = arguments[1];
options = {};
}
if (!this._isValidKey(key)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof options !== 'object') {
throw new TypeError('Second argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Third argument needs to be a callback.');
}
this._checkHashkeyOption(options);
this._checkCasOption(options);
this._checkDuraOptions(options);
this._maybeInvoke(this._cb.remove,
[key, options.hashkey, options.cas,
this._interceptEndure(key, options, 1, callback)]);
};
/**
* Performs a storage operation. This is a single handler function for all
* possible storage operations. This is thanks to libcouchbase handling them
* all as a single entity as well.
*
* @param {string|Buffer} key
* @param {*} value
* @param {Object} [options]
* @param {Bucket.OpCallback} callback
* @param {number} opType
*
* @private
* @ignore
*/
Bucket.prototype._store = function(key, value, options, callback, opType) {
if (options instanceof Function) {
callback = arguments[2];
options = {};
}
if (!this._isValidKey(key)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (value === undefined) {
throw new TypeError('Second argument must not be undefined.');
}
if (typeof options !== 'object') {
throw new TypeError('Third argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Fourth argument needs to be a callback.');
}
this._checkHashkeyOption(options);
this._checkExpiryOption(options);
this._checkCasOption(options);
this._checkDuraOptions(options);
this._maybeInvoke(this._cb.store,
[key, options.hashkey, value, options.expiry, options.cas, opType,
this._interceptEndure(key, options, 0, callback)]);
};
/**
* Stores a document to the bucket.
*
* @param {string|Buffer} key
* The target document key.
* @param {!*} value
* The document's contents.
* @param {Object} [options]
* @param {Bucket.CAS} [options.cas=undefined]
* The CAS value to check. If the item on the server contains a different
* CAS value, the operation will fail. Note that if this option is undefined,
* no comparison will be performed.
* @param {number} [options.expiry=0]
* Set the initial expiration time for the document. A value of 0 represents
* never expiring.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.upsert = function(key, value, options, callback) {
this._store(key, value, options, callback, CONST.SET);
};
/**
* Identical to {@link Bucket#upsert} but will fail if the document already
* exists.
*
* @param {string|Buffer} key
* The target document key.
* @param {!*} value
* The document's contents.
* @param {Object} [options]
* @param {number} [options.expiry=0]
* Set the initial expiration time for the document. A value of 0 represents
* never expiring.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.insert = function(key, value, options, callback) {
this._store(key, value, options, callback, CONST.ADD);
};
/**
* Identical to {@link Bucket#set}, but will only succeed if the document
* exists already (i.e. the inverse of {@link Bucket#add}).
*
* @param {string|Buffer} key
* The target document key.
* @param {!*} value
* The document's contents.
* @param {Object} [options]
* @param {Bucket.CAS} [options.cas=undefined]
* The CAS value to check. If the item on the server contains a different
* CAS value, the operation will fail. Note that if this option is undefined,
* no comparison will be performed.
* @param {number} [options.expiry=0]
* Set the initial expiration time for the document. A value of 0 represents
* never expiring.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.replace = function(key, value, options, callback) {
this._store(key, value, options, callback, CONST.REPLACE);
};
/**
* Similar to {@link Bucket#set}, but instead of setting a new key,
* it appends data to the existing key. Note that this function only makes
* sense when the stored data is a string; 'appending' to a JSON document may
* result in parse errors when the document is later retrieved.
*
* @param {string|Buffer} key
* The target document key.
* @param {!*} fragment
* The document's contents to append.
* @param {Object} [options]
* @param {Bucket.CAS} [options.cas=undefined]
* The CAS value to check. If the item on the server contains a different
* CAS value, the operation will fail. Note that if this option is undefined,
* no comparison will be performed.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @see Bucket#prepend
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.append = function(key, fragment, options, callback) {
this._store(key, fragment, options, callback, CONST.APPEND);
};
/**
* Like {@linkcode Bucket#append}, but prepends data to the existing value.
*
* @param {string|Buffer} key
* The target document key.
* @param {!*} fragment
* The document's contents to prepend.
* @param {Object} [options]
* @param {Bucket.CAS} [options.cas=undefined]
* The CAS value to check. If the item on the server contains a different
* CAS value, the operation will fail. Note that if this option is undefined,
* no comparison will be performed.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @see Bucket#append
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.prepend = function(key, fragment, options, callback) {
this._store(key, fragment, options, callback, CONST.PREPEND);
};
/**
* Increments or decrements a key's numeric value.
*
* Note that JavaScript does not support 64-bit integers (while libcouchbase
* and the server do). You might receive an inaccurate value if the
* number is greater than 53-bits (JavaScript's maximum integer precision).
*
* @param {string|Buffer} key
* The target document key.
* @param {number} delta
* The amount to add or subtract from the counter value. This value may be
* any non-zero integer.
* @param {Object} [options]
* @param {number} [options.initial=undefined]
* Sets the initial value for the document if it does not exist. Specifying
* a value of undefined will cause the operation to fail if the document
* does not exist, otherwise this value must be equal to or greater than 0.
* @param {number} [options.expiry=0]
* Set the initial expiration time for the document. A value of 0 represents
* never expiring.
* @param {number} [options.persist_to=0]
* Ensures this operation is persisted to this many nodes
* @param {number} [options.replicate_to=0]
* Ensures this operation is replicated to this many nodes
* @param {Bucket.OpCallback} callback
*
* @since 2.0.0
* @committed
*/
Bucket.prototype.counter = function(key, delta, options, callback) {
if (options instanceof Function) {
callback = arguments[2];
options = {};
}
if (!this._isValidKey(key)) {
throw new TypeError('First argument needs to be a string or buffer.');
}
if (typeof delta !== 'number' || delta === 0) {
throw new TypeError('Second argument must be a non-zero integer.');
}
if (typeof options !== 'object') {
throw new TypeError('Third argument needs to be an object or callback.');
}
if (typeof callback !== 'function') {
throw new TypeError('Fourth argument needs to be a callback.');
}
if (options.initial) {
if (typeof options.initial !== 'number' || options.initial < 0) {
throw new TypeError('initial option must be 0 or a positive integer.');
}
}
this._checkHashkeyOption(options);
this._checkExpiryOption(options);
this._checkDuraOptions(options);
this._maybeInvoke(this._cb.arithmetic,
[key, options.hashkey, options.expiry, delta, options.initial,
this._interceptEndure(key, options, 0, callback)]);
};
/**
* Gets or sets the operation timeout in milliseconds. The operation timeout
* is the time that Bucket will wait for a response from the server for a CRUD
* operation. If the response is not received within this time frame, the
* operation is failed with an error.
*
* @member {number} Bucket#operationTimeout
* @default 2500
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'operationTimeout', {
get: function() {
return this._ctl(CONST.CNTL_OP_TIMEOUT);
},
set: function(val) {
this._ctl(CONST.CNTL_OP_TIMEOUT, val);
}
});
/**
* Gets or sets the view timeout in milliseconds. The view timeout is the
* time that Bucket will wait for a response from the server for a view request.
* If the response is not received within this time frame, the request fails
* with an error.
*
* @member {number} Bucket#viewTimeout
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'viewTimeout', {
get: function() {
return this._ctl(CONST.CNTL_VIEW_TIMEOUT);
},
set: function(val) {
this._ctl(CONST.CNTL_VIEW_TIMEOUT, val);
}
});
/**
* Gets or sets the durability timeout in milliseconds. The durability timeout
* is the time that Bucket will wait for a response from the server in regards
* to a durability request. If there are no responses received within this time
* frame, the request fails with an error.
*
* @member {number} Bucket#durabilityTimeout
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'durabilityTimeout', {
get: function() {
return this._ctl(CONST.CNTL_DURABILITY_TIMEOUT);
},
set: function(val) {
this._ctl(CONST.CNTL_DURABILITY_TIMEOUT, val);
}
});
/**
* Gets or sets the durability interval in milliseconds. The durability
* interval is the time that Bucket will wait between requesting new durability
* information during a durability poll.
*
* @member {number} Bucket#durabilityInterval
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'durabilityInterval', {
get: function() {
return this._ctl(CONST.CNTL_DURABILITY_INTERVAL);
},
set: function(val) {
this._ctl(CONST.CNTL_DURABILITY_INTERVAL, val);
}
});
/**
* Gets or sets the management timeout in milliseconds. The management timeout
* is the time that Bucket will wait for a response from the server for a
* management request. If the response is not received within this time frame,
* the request is failed out with an error.
*
* @member {number} Bucket#managementTimeout
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'managementTimeout', {
get: function() {
return this._ctl(CONST.CNTL_HTTP_TIMEOUT);
},
set: function(val) {
this._ctl(CONST.CNTL_HTTP_TIMEOUT, val);
}
});
/**
* Gets or sets the config throttling in milliseconds. The config throttling is
* the time that Bucket will wait before forcing a configuration refresh. If no
* refresh occurs before this period while a configuration is marked invalid,
* an update will be triggered.
*
* @member {number} Bucket#configThrottle
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'configThrottle', {
get: function() {
return this._ctl(CONST.CNTL_CONFDELAY_THRESH);
},
set: function(val) {
this._ctl(CONST.CNTL_CONFDELAY_THRESH, val);
}
});
/**
* Sets or gets the connection timeout in milliseconds. This is the timeout
* value used when connecting to the configuration port during the initial
* connection (in this case, use this as a key in the 'options' parameter in
* the constructor) and/or when Bucket attempts to reconnect in-situ (if the
* current connection has failed).
*
* @member {number} Bucket#connectionTimeout
* @default 5000
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'connectionTimeout', {
get: function() {
return this._ctl(CONST.CNTL_CONFIGURATION_TIMEOUT );
},
set: function(val) {
this._ctl(CONST.CNTL_CONFIGURATION_TIMEOUT, val);
}
});
/**
* Sets or gets the node connection timeout in msecs. This value is similar to
* {@link Bucket#connectionTimeout}, but defines the time to wait for a
* particular node to respond before trying the next one.
*
* @member {number} Bucket#nodeConnectionTimeout
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'nodeConnectionTimeout', {
get: function() {
return this._ctl(CONST.CNTL_CONFIG_NODE_TIMEOUT );
},
set: function(val) {
this._ctl(CONST.CNTL_CONFIG_NODE_TIMEOUT, val);
}
});
/**
* Returns the libcouchbase version as a string. This information will usually
* be in the format of 2.4.0-fffffff representing the major, minor, patch and
* git-commit that the built libcouchbase is based upon.
*
* @member {string} Bucket#lcbVersion
*
* @example
* "2.4.0-beta.adbf222"
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'lcbVersion', {
get: function() {
return this._cb.lcbVersion();
},
writeable: false
});
/**
* Returns the version of the Node.js library as a string.
*
* @member {string} Bucket#clientVersion
*
* @example
* "2.0.0-beta.fa123bd"
*
* @since 2.0.0
* @committed
*/
Object.defineProperty(Bucket.prototype, 'clientVersion', {
get: function() {
var pkgJson = fs.readFileSync(
path.resolve(__dirname, '../package.json'));
return JSON.parse(pkgJson).version;
},
writeable: false
});
module.exports = Bucket;