markin-couchbase
Version:
Markin Fork of Couchbase Node.js Client Library.
1,334 lines (1,206 loc) • 37.2 kB
JavaScript
'use strict';
var events = require('events');
var util = require('util');
var ViewQuery = require('../viewquery');
var SpatialQuery = require('../spatialquery');
var N1qlQuery = require('../n1qlquery');
var BucketManager = require('./bucketmgr');
var CbError = require('./error');
var errs = require('../errors');
// Mock version should match the latest fully supported version of couchnode.
var MOCK_VERSION = '2.0.0';
function MockCouchbaseCas(idx) {
this.x = idx;
}
MockCouchbaseCas.prototype.toString = function() {
return this.x.toString(10);
};
MockCouchbaseCas.prototype.toJSON = function() {
return this.x.toString(10);
};
MockCouchbaseCas.prototype.inspect = function() {
return 'MockCouchbaseCas<' + this.x + '>';
};
var casCounter = 0;
/* istanbul ignore next */
function _createCas() {
return new MockCouchbaseCas(casCounter++);
}
/* istanbul ignore next */
function _compareCas(a, b) {
if (!b) {
return true;
} else if (!a && b) {
return false;
} else {
if (typeof a === 'string') {
a = {x: parseInt(a, 10)};
}
if (typeof b === 'string') {
b = {x: parseInt(b, 10)};
}
return a.x === b.x;
}
}
/* istanbul ignore next */
function _makeExpiryDate(expiry) {
if (!expiry) {
return null;
}
if (expiry < 30*24*60*60) {
var dt = new Date();
dt.setTime(dt.getTime() + (expiry*1000));
return dt;
} else {
return new Date(expiry * 1000);
}
}
/* istanbul ignore next */
function _makeLockDate(lockTime) {
var dt = new Date();
dt.setTime(dt.getTime() + (lockTime*1000));
return dt;
}
var FLAGS = {
// Node Flags - Formats
NF_JSON: 0x00,
NF_RAW: 0x02,
NF_UTF8: 0x04,
NF_MASK: 0xFF
};
function _defaultEncode(doc) {
if (typeof doc === 'string') {
return {
flags: FLAGS.NF_UTF8,
value: new Buffer(doc, 'utf8')
};
} else if (Buffer.isBuffer(doc)) {
return {
flags: FLAGS.NF_RAW,
value: new Buffer(doc)
};
} else {
return {
flags: FLAGS.NF_JSON,
value: new Buffer(JSON.stringify(doc), 'utf8')
};
}
}
function _defaultDecode(info) {
if (info.flags === FLAGS.NF_UTF8) {
return info.value.toString('utf8');
} else if (info.flags === FLAGS.NF_RAW) {
return new Buffer(info.value);
} else if (info.flags === FLAGS.NF_JSON) {
return JSON.parse(info.value.toString('utf8'));
} else {
return new Buffer(info.value);
}
}
/* istanbul ignore next */
function MockStorage() {
this.items = {};
}
/* istanbul ignore next */
MockStorage.prototype._encodeKey = function(key) {
return key;
};
/* istanbul ignore next */
MockStorage.prototype.get = function(key, hashkey) {
if (!hashkey) {
throw new Error('invalid hashkey');
}
var keyItem = this.items[this._encodeKey(key)];
if (!keyItem) {
return null;
}
var item = keyItem[this._encodeKey(hashkey)];
if (!item) {
return null;
}
if (item.expiry && item.expiry <= new Date()) {
return null;
}
return item;
};
/* istanbul ignore next */
MockStorage.prototype.set = function(key, hashkey, item) {
if (!hashkey) {
throw new Error('invalid hashkey');
}
var keyE = this._encodeKey(key);
var hashkeyE = this._encodeKey(hashkey);
if (!this.items[keyE]) {
this.items[keyE] = {};
}
this.items[keyE][hashkeyE] = item;
};
/* istanbul ignore next */
MockStorage.prototype.remove = function(key, hashkey) {
if (!hashkey) {
throw new Error('invalid hashkey');
}
var keyE = this._encodeKey(key);
var hashkeyE = this._encodeKey(hashkey);
if (!this.items[keyE]) {
return null;
}
delete this.items[keyE][hashkeyE];
return _createCas();
};
function MockBucket(options) {
this.storage = new MockStorage();
this.ddocs = {};
this.operationTimeout = 2500;
this.viewTimeout = 2500;
this.durabilityTimeout = 2500;
this.durabilityInterval = 2500;
this.managementTimeout = 2500;
this.configThrottle = 2500;
this.connectionTimeout = 2500;
this.nodeConnectionTimeout = 2500;
this._encodeDoc = _defaultEncode;
this._decodeDoc = _defaultDecode;
var self = this;
var connOpts = options.dsnObj;
this.connected = null;
process.nextTick(function() {
if (connOpts.bucket !== 'invalid_bucket') {
self.connected = true;
self.emit('connect');
} else {
self.connected = false;
self.emit('error', new Error('invalid bucket name'));
}
});
this.waitQueue = [];
this.on('connect', function() {
for (var i = 0; i < this.waitQueue.length; ++i) {
this.waitQueue[i][0].call(this);
}
this.waitQueue = [];
});
this.on('error', function(err) {
for (var i = 0; i < this.waitQueue.length; ++i) {
this.waitQueue[i][1].call(this, err, null);
}
this.waitQueue = [];
});
}
util.inherits(MockBucket, events.EventEmitter);
MockBucket.prototype.dump = function() {
var out = {
data: {},
ddocs: this.ddocs
};
var keyspace = this.storage.items;
for (var i in keyspace) {
if (keyspace.hasOwnProperty(i)) {
var hashspace = keyspace[i];
if (hashspace[i]) {
var data = hashspace[i];
out.data[i] = {
value: this._decodeDoc({value:data.value,flags:data.flags}),
flags: data.flags,
expiry: data.expiry,
cas: data.cas
};
}
}
}
console.log(util.inspect(out, {depth: 16}));
};
/* istanbul ignore next */
MockBucket.prototype.enableN1ql = function(uri) {
throw new Error('not supported on mock');
};
MockBucket.prototype.manager = function() {
return new BucketManager(this);
};
MockBucket.prototype.disconnect = function() {
this.connected = false;
};
MockBucket.prototype.setTranscoder = function(encoder, decoder) {
if (encoder) {
this._encodeDoc = encoder;
} else {
this._encodeDoc = _defaultEncode;
}
if (decoder) {
this._decodeDoc = decoder;
} else {
this._decodeDoc = _defaultDecode;
}
};
MockBucket.prototype._maybeInvoke = function(fn, callback) {
if (this.connected === true) {
setImmediate(fn.bind(this));
} else if (this.connected === false) {
throw new Error('cannot perform operations on a shutdown bucket');
} else {
this.waitQueue.push([fn, callback]);
}
};
MockBucket.prototype._isValidKey = function(key) {
return typeof key === 'string' || key instanceof Buffer;
};
MockBucket.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.');
}
}
};
MockBucket.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.');
}
}
};
MockBucket.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.');
}
}
};
MockBucket.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.');
}
}
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
// If the key is locked, the server actually responds with a -1 cas value
// which is considered special, here we just make a fake cas.
callback(null, {
value: decValue,
cas: _createCas()
});
} else {
callback(null, {
value: decValue,
cas: origItem.cas
});
}
}, callback);
};
MockBucket.prototype.getMulti = function(keys, callback) {
if (!Array.isArray(keys) || keys.length === 0) {
throw new TypeError('First argument needs to be an array of non-zero length.');
}
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]);
}
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
origItem.expiry = _makeExpiryDate(expiry);
var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
callback(null, {
value: decValue,
cas: origItem.cas
});
}, callback);
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
if (options.lockTime) {
origItem.lockExpiry = _makeLockDate(options.lockTime);
} else {
origItem.lockExpiry = _makeLockDate(15);
}
origItem.cas = _createCas();
var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
callback(null, {
value: decValue,
cas: origItem.cas
});
}, callback);
};
MockBucket.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);
/* istanbul ignore next */
this._maybeInvoke(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
callback(null, {
value: decValue,
cas: origItem.cas
});
}, callback);
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
origItem.expiry = _makeExpiryDate(expiry);
callback(null, {
cas: origItem.cas
});
}, callback);
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
if (!_compareCas(origItem.cas, cas)) {
return callback(new CbError(
'cas does not match', errs.keyAlreadyExists), null);
}
if (!origItem.lockExpiry || origItem.lockExpiry <= new Date()) {
return callback(new CbError(
'key not locked', errs.temporaryError), null);
}
origItem.lockExpiry = null;
callback(null, {
cas: origItem.cas
});
}, callback);
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origItem = this.storage.get(key, options.hashkey);
if (!origItem) {
return callback(new CbError('key not found', errs.keyNotFound), null);
}
if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
var delCas = this.storage.remove(key, options.hashkey);
callback(null, {
cas: delCas
});
}, callback);
};
MockBucket.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(function() {
/*
The following is intentionally verbose and repeated to make it
easier to see test-coverage of different paths that would occur
on the server.
*/
if (opType === 'set') {
if (!options.hashkey) {
options.hashkey = key;
}
var origSetItem = this.storage.get(key, options.hashkey);
if (origSetItem && origSetItem.lockExpiry &&
origSetItem.lockExpiry > new Date() && !options.cas) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
if (origSetItem && !_compareCas(origSetItem.cas, options.cas)) {
return callback(new CbError(
'cas mismatch', key.keyAlreadyExists), null);
}
var encItemSet = this._encodeDoc(value);
var newSetItem = {
value: encItemSet.value,
flags: encItemSet.flags,
expiry: _makeExpiryDate(options.expiry),
cas: _createCas()
};
this.storage.set(key, options.hashkey, newSetItem);
callback(null, {cas: newSetItem.cas});
} else if (opType === 'add') {
if (!options.hashkey) {
options.hashkey = key;
}
var origAddItem = this.storage.get(key, options.hashkey);
if (origAddItem) {
return callback(new CbError(
'key already exists', errs.keyAlreadyExists), null);
}
var encItemAdd = this._encodeDoc(value);
var newAddItem = {
value: encItemAdd.value,
flags: encItemAdd.flags,
expiry: _makeExpiryDate(options.expiry),
cas: _createCas()
};
this.storage.set(key, options.hashkey, newAddItem);
callback(null, {cas: newAddItem.cas});
} else if (opType === 'replace') {
if (!options.hashkey) {
options.hashkey = key;
}
var origReplaceItem = this.storage.get(key, options.hashkey);
if (!origReplaceItem) {
return callback(new CbError(
'key does not exist', errs.keyNotFound), null);
}
if (origReplaceItem.lockExpiry && origReplaceItem.lockExpiry > new Date() && !options.cas) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
if (origReplaceItem && !_compareCas(origReplaceItem.cas, options.cas)) {
return callback(new CbError(
'cas mismatch', errs.keyAlreadyExists), null);
}
var encItemReplace = this._encodeDoc(value);
var newReplaceItem = {
value: encItemReplace.value,
flags: encItemReplace.flags,
expiry: _makeExpiryDate(options.expiry),
cas: _createCas()
};
this.storage.set(key, options.hashkey, newReplaceItem);
callback(null, {cas: newReplaceItem.cas});
} else if (opType === 'append') {
if (!options.hashkey) {
options.hashkey = key;
}
var origAppendItem = this.storage.get(key, options.hashkey);
if (!origAppendItem) {
return callback(new CbError(
'key does not exist', errs.keyNotFound), null);
}
if (origAppendItem.lockExpiry && origAppendItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
var encValAppend = this._encodeDoc(value);
origAppendItem.value = Buffer.concat([
origAppendItem.value, encValAppend.value]);
origAppendItem.cas = _createCas();
callback(null, {
cas: origAppendItem.cas
});
} else if (opType === 'prepend') {
if (!options.hashkey) {
options.hashkey = key;
}
var origPrependItem = this.storage.get(key, options.hashkey);
if (!origPrependItem) {
return callback(new CbError(
'key does not exist', errs.keyNotFound), null);
}
if (origPrependItem.lockExpiry && origPrependItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
var encValPrepend = this._encodeDoc(value);
origPrependItem.value = Buffer.concat([
encValPrepend.value, origPrependItem.value]);
origPrependItem.cas = _createCas();
callback(null, {
cas: origPrependItem.cas
});
}
}, callback);
};
MockBucket.prototype.upsert = function(key, value, options, callback) {
this._store(key, value, options, callback, 'set');
};
MockBucket.prototype.insert = function(key, value, options, callback) {
this._store(key, value, options, callback, 'add');
};
MockBucket.prototype.replace = function(key, value, options, callback) {
this._store(key, value, options, callback, 'replace');
};
MockBucket.prototype.append = function(key, fragment, options, callback) {
this._store(key, fragment, options, callback, 'append');
};
MockBucket.prototype.prepend = function(key, fragment, options, callback) {
this._store(key, fragment, options, callback, 'prepend');
};
MockBucket.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(function() {
if (!options.hashkey) {
options.hashkey = key;
}
var origCountItem = this.storage.get(key, options.hashkey);
if (options.initial !== undefined && !origCountItem) {
var newCountItem = {
value: new Buffer(options.initial.toString(), 'utf8'),
flags: 0,
cas: _createCas()
};
this.storage.set(key, options.hashkey, newCountItem);
return callback(null, {
value: options.initial,
cas: newCountItem.cas
});
}
if (!origCountItem) {
return callback(new CbError(
'key does not exist', errs.keyNotFound), null);
}
if (origCountItem.lockExpiry && origCountItem.lockExpiry > new Date()) {
return callback(new CbError(
'temporary error - key locked', errs.temporaryError), null);
}
var strValue = origCountItem.value.toString('utf8');
var numValue = parseInt(strValue, 10);
numValue += delta;
origCountItem.value = new Buffer(numValue.toString(), 'utf8');
origCountItem.cas = _createCas();
callback(null, {
value: numValue,
cas: origCountItem.cas
});
}, callback);
};
MockBucket.prototype.query = function(query, params, callback) {
if (params instanceof Function) {
callback = arguments[1];
params = undefined;
}
if (query instanceof ViewQuery) {
return this._view(query.ddoc, query.name, query.options, callback);
} else if (query instanceof SpatialQuery) {
throw new Error('Spatial queries are not supported in the mock.');
} else if (query instanceof N1qlQuery) {
throw new Error('N1QL queries are not supported in the mock.');
} else {
throw new TypeError(
'First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.');
}
};
Object.defineProperty(MockBucket.prototype, 'lcbVersion', {
get: function() {
return '0.0.0';
},
writeable: false
});
Object.defineProperty(MockBucket.prototype, 'clientVersion', {
get: function() {
return MOCK_VERSION;
},
writeable: false
});
function ViewQueryResponse(req) {
}
util.inherits(ViewQueryResponse, events.EventEmitter);
MockBucket.prototype._view = function(ddoc, name, q, callback) {
var req = new ViewQueryResponse();
var self = this;
process.nextTick(function() {
self._execView(ddoc, name, q, function(err, rows, meta) {
if (err) {
return req.emit('error', err);
}
for (var i = 0; i < rows.length; ++i) {
req.emit('row', rows[i]);
}
req.emit('rows', rows, meta);
req.emit('end', meta);
});
});
if (callback) {
req.on('rows', function(rows, meta) {
callback(null, rows, meta);
});
req.on('error', function(err) {
callback(err, null, null);
});
}
return req;
};
MockBucket.prototype._indexView = function(ddoc, name, options, callback) {
var ddocObj = this.ddocs[ddoc];
if (!ddocObj) {
return callback(new Error('not_found'));
}
if (ddocObj.views) {
ddocObj = ddocObj.views;
}
var viewObj = ddocObj[name];
if (!viewObj) {
return callback(new Error('not_found'));
}
var viewMapFunc = viewObj.map;
var retvals = [];
var curdocval = null;
var curdockey = null;
var curdocmeta = null;
function emit(key, val) {
var row = {
key: key,
id: curdockey,
value: val,
doc: {
meta: curdocmeta
}
};
if (curdocmeta.type === 'json') {
row.doc.json = curdocval;
} else {
row.doc.base64 = curdocval;
}
retvals.push(row);
}
var procOne = function(doc,meta){};
eval('procOne = ' + viewMapFunc);
for (var keyName in this.storage.items) {
if (this.storage.items.hasOwnProperty(keyName)) {
var thisKey = this.storage.items[keyName];
for (var hashKey in thisKey) {
if (thisKey.hasOwnProperty(hashKey)) {
var thisVal = thisKey[hashKey];
curdockey = keyName;
curdocval = null;
try {
curdocval = JSON.parse(thisVal.value.toString());
} catch (e) {
}
var curdoctype = curdocval ? 'json' : 'base64';
if (!curdocval) {
curdocval = thisVal.value.toString('base64');
}
curdocmeta = {
id: curdockey,
rev: '?NOTVALIDFORMOCK?',
expiration: thisVal.expiry ? thisVal.expiry : 0,
flags: thisVal.flags,
type: curdoctype
};
procOne(curdocval, curdocmeta);
}
}
}
}
var reducer = viewObj.reduce;
if (reducer) {
if (reducer === '_count') {
reducer = function(key, values, rereduce) {
if (rereduce) {
var result = 0;
for (var i = 0; i < values.length; i++) {
result += values[i];
}
return result;
} else {
return values.length;
}
};
} else if (reducer === '_sum') {
reducer = function(key, values, rereduce) {
var sum = 0;
for(var i = 0; i < values.length; i++) {
sum = sum + values[i];
}
return(sum);
};
} else if (reducer === '_stats') {
reducer = function(key, values, rereduce) {
return null;
};
} else {
eval('reducer = ' + reducer);
}
}
callback(null, retvals, reducer);
};
// http://docs.couchdb.org/en/latest/couchapp/views/collation.html
var SORT_ORDER = function() {
var ordered_array = [
'null',
'false',
'true',
'number',
'string',
'array',
'object',
'unknown'
];
var obj = {};
for (var i = 0; i < ordered_array.length; i++) {
obj[ordered_array[i]] = i;
}
return obj;
}();
/**
* Returns the sorting priority for a given type
* @param v The value whose type should be evaluated
* @return The numeric sorting index
*/
function getSortIndex(v) {
if (typeof v === 'string') {
return SORT_ORDER['string'];
} else if (typeof v === 'number') {
return SORT_ORDER['number'];
} else if (Array.isArray(v)) {
return SORT_ORDER['array'];
} else if (v === true) {
return SORT_ORDER['true'];
} else if (v === false) {
return SORT_ORDER['false'];
} else if (typeof v === 'object') {
return SORT_ORDER['object'];
} else {
return SORT_ORDER['unknown'];
}
}
/**
* Compares one value with another
* @param a The first value
* @param b The second value
* @param [exact] If both @c b and @c b are arrays, setting this parameter to true
* ensures that they will only be equal if their length matches and their
* contents match. If this value is false (the default), then only the common
* subset of elements are evaluated
*
* @return {number} greater than 0 if @c a is bigger than @b; a number less
* than 0 if @a is less than @b, or 0 if they are equal
*/
function cbCompare(a, b, exact) {
if (Array.isArray(a) && Array.isArray(b)) {
if (exact === true) {
if (a.length !== b.length) {
return a.length > b.length ? +1 : -1;
}
}
var maxLength = a.length > b.length ? b.length : a.length;
for (var i = 0; i < maxLength; ++i) {
var subCmp = cbCompare(a[i], b[i], true);
if (subCmp !== 0) {
return subCmp;
}
}
return 0;
}
if (typeof a === 'string' && typeof b === 'string') {
return a.localeCompare(b);
}
if (typeof a === 'number' && typeof b === 'number') {
return a - b;
}
// Now we need to do special things
var aPriority = getSortIndex(a);
var bPriority = getSortIndex(b);
if (aPriority !== bPriority) {
return aPriority - bPriority;
} else {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
}
/**
* Find the index of @c val in the array @arr
* @param arr The array to search in
* @param val The value to search for
* @return {number} the index in the array, or -1 if the item does not exist
*/
function cbIndexOf(arr, val) {
for (var i = 0; i < arr.length; ++i) {
if (cbCompare(arr[i], val, true) === 0) {
return i;
}
}
return -1;
}
/**
* Normalize a key for reduce
* @param key The key to normalize
* @param groupLevel The group level
* @return {*}
*/
function cbNormKey(key, groupLevel) {
if (groupLevel === 0) {
return null;
}
if (Array.isArray(key)) {
if (groupLevel === -1) {
return key;
} else {
return key.slice(0, groupLevel);
}
} else {
return key;
}
}
MockBucket.prototype._execView = function(ddoc, name, options, callback) {
this._indexView(ddoc, name, options, function(err, results, reducer) {
if (err) {
return callback(err);
}
// Store total emitted rows
var rowcount = results.length;
// Parse if needed
var startkey = options.startkey ? JSON.parse(options.startkey) : undefined;
var startkey_docid = options.startkey_docid ? JSON.parse(options.startkey_docid) : undefined;
var endkey = options.endkey ? JSON.parse(options.endkey) : undefined;
var endkey_docid = options.endkey_docid ? JSON.parse(options.endkey_docid) : undefined;
var group_level = options.group_level ? options.group_level : 0;
var inclusive_start = true;
var inclusive_end = true;
if (options.inclusive_end !== undefined) {
inclusive_end = options.inclusive_end;
}
// Invert if descending
if (options.descending) {
var _startkey = startkey;
startkey = endkey;
endkey = _startkey;
var _startkey_docid = startkey_docid;
startkey_docid = endkey_docid;
endkey_docid = _startkey_docid;
var _inclusive_start = inclusive_start;
inclusive_start = inclusive_end;
inclusive_end = _inclusive_start;
}
var key = options.key ? JSON.parse(options.key) : undefined;
var keys = options.keys ? JSON.parse(options.keys) : undefined;
var newResults = [];
for (var i = 0; i < results.length; ++i) {
var dockey = results[i].key;
var docid = results[i].id;
if (key !== undefined) {
if (cbCompare(dockey, key) !== 0) {
continue;
}
}
if (keys !== undefined) {
if (cbIndexOf(keys, dockey) < 0) {
continue;
}
}
if (inclusive_start) {
if (startkey && cbCompare(dockey, startkey) < 0) {
continue;
}
if (startkey_docid && cbCompare(docid, startkey_docid) < 0) {
continue;
}
} else {
if (startkey && cbCompare(dockey, startkey) <= 0) {
continue;
}
if (startkey_docid && cbCompare(docid, startkey_docid) <= 0) {
continue;
}
}
if (inclusive_end) {
if (endkey && cbCompare(dockey, endkey) > 0) {
continue;
}
if (endkey_docid && cbCompare(docid, endkey_docid) > 0) {
continue;
}
} else {
if (endkey && cbCompare(dockey, endkey) >= 0) {
continue;
}
if (endkey_docid && cbCompare(docid, endkey_docid) >= 0) {
continue;
}
}
if (!options.include_docs) {
delete results[i].doc;
}
newResults.push(results[i]);
}
results = newResults;
if (options.descending) {
results.sort(function(a,b){
if (a.key > b.key) { return -1; }
if (a.key < b.key) { return +1; }
return 0;
});
} else {
results.sort(function(a,b){
if (b.key > a.key) { return -1; }
if (b.key < a.key) { return +1; }
return 0;
});
}
if (options.skip && options.limit) {
results = results.slice(options.skip, options.skip + options.limit);
} else if (options.skip) {
results = results.slice(options.skip);
} else if (options.limit) {
results = results.slice(0, options.limit);
}
// Reduce Time!!
if (reducer && options.reduce !== false) {
var keys = [];
for (var i = 0; i < results.length; ++i) {
var keyN = cbNormKey(results[i].key, group_level);
if (cbIndexOf(keys, keyN) < 0) {
keys.push(keyN);
}
}
var newResults = [];
for (var j = 0; j < keys.length; ++j) {
var values = [];
for (var k = 0; k < results.length; ++k) {
var keyN = cbNormKey(results[k].key, group_level);
if (cbCompare(keyN, keys[j]) === 0) {
values.push(results[k].value);
}
}
var result = reducer(keys[j], values, false);
newResults.push({
key: keys[j],
value: result
});
}
results = newResults;
}
var meta = {
total_rows: rowcount
};
callback(null, results, meta);
});
};
module.exports = MockBucket;