marsdb
Version:
MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6
567 lines (511 loc) • 20.1 kB
JavaScript
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; /**
* Based on Meteor's EJSON package.
* Rewrite with ES6 and better formated for passing
* linter
*/
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.EJSON = undefined;
var _Base = require('./Base64');
var _Base2 = _interopRequireDefault(_Base);
var _some2 = require('fast.js/array/some');
var _some3 = _interopRequireDefault(_some2);
var _checkTypes = require('check-types');
var _checkTypes2 = _interopRequireDefault(_checkTypes);
var _keys2 = require('fast.js/object/keys');
var _keys3 = _interopRequireDefault(_keys2);
var _forEach = require('fast.js/forEach');
var _forEach2 = _interopRequireDefault(_forEach);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// Internal utils
function _isNaN(val) {
return typeof val === 'number' && val != +val;
}
function _has(obj, key) {
return _checkTypes2.default.object(obj) && obj.hasOwnProperty(key);
}
function _isInfOrNan(val) {
return _isNaN(val) || val === Infinity || val === -Infinity;
}
function _isArguments(val) {
return !!val && (typeof val === 'undefined' ? 'undefined' : _typeof(val)) == 'object' && Object.prototype.hasOwnProperty.call(val, 'callee') && !Object.prototype.propertyIsEnumerable.call(val, 'callee');
}
var EJSON = exports.EJSON = function () {
// @ngInject
function EJSON() {
_classCallCheck(this, EJSON);
this._setupBuiltinConverters();
this._customTypes = {};
}
/**
* @summary Add a custom type, using a method of your choice to get to and
* from a basic JSON-able representation. The factory argument
* is a function of JSON-able --> your object
* The type you add must have:
* - A toJSONValue() method, so that Meteor can serialize it
* - a typeName() method, to show how to look it up in our type table.
* It is okay if these methods are monkey-patched on.
* EJSON.clone will use toJSONValue and the given factory to produce
* a clone, but you may specify a method clone() that will be used instead.
* Similarly, EJSON.equals will use toJSONValue to make comparisons,
* but you may provide a method equals() instead.
* @locus Anywhere
* @param {String} name A tag for your custom type; must be unique among custom data types defined in your project, and must match the result of your type's `typeName` method.
* @param {Function} factory A function that deserializes a JSON-compatible value into an instance of your type. This should match the serialization performed by your type's `toJSONValue` method.
*/
_createClass(EJSON, [{
key: 'addType',
value: function addType(name, factory) {
if (_has(this._customTypes, name)) {
throw new Error('Type ' + name + ' already present');
}
this._customTypes[name] = factory;
}
/**
* @summary Serialize an EJSON-compatible value into its plain JSON representation.
* @locus Anywhere
* @param {EJSON} val A value to serialize to plain JSON.
*/
}, {
key: 'toJSONValue',
value: function toJSONValue(item) {
var changed = this._toJSONValueHelper(item);
if (changed !== undefined) {
return changed;
}
if ((typeof item === 'undefined' ? 'undefined' : _typeof(item)) === 'object') {
item = this.clone(item);
this._adjustTypesToJSONValue(item);
}
return item;
}
/**
* @summary Deserialize an EJSON value from its plain JSON representation.
* @locus Anywhere
* @param {JSONCompatible} val A value to deserialize into EJSON.
*/
}, {
key: 'fromJSONValue',
value: function fromJSONValue(item) {
var changed = this._fromJSONValueHelper(item);
if (changed === item && (typeof item === 'undefined' ? 'undefined' : _typeof(item)) === 'object') {
item = this.clone(item);
this._adjustTypesFromJSONValue(item);
return item;
} else {
return changed;
}
}
/**
* @summary Serialize a value to a string.
* For EJSON values, the serialization fully represents the value. For non-EJSON values, serializes the same way as `JSON.stringify`.
* @locus Anywhere
* @param {EJSON} val A value to stringify.
*/
}, {
key: 'stringify',
value: function stringify(item) {
var json = this.toJSONValue(item);
return JSON.stringify(json);
}
/**
* @summary Parse a string into an EJSON value. Throws an error if the string is not valid EJSON.
* @locus Anywhere
* @param {String} str A string to parse into an EJSON value.
*/
}, {
key: 'parse',
value: function parse(item) {
if (typeof item !== 'string') {
throw new Error('EJSON.parse argument should be a string');
}
return this.fromJSONValue(JSON.parse(item));
}
/**
* @summary Returns true if `x` is a buffer of binary data, as returned from [`EJSON.newBinary`](#ejson_new_binary).
* @param {Object} x The variable to check.
* @locus Anywhere
*/
}, {
key: 'isBinary',
value: function isBinary(obj) {
return !!(typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array || obj && obj.$Uint8ArrayPolyfill);
}
/**
* @summary Return true if `a` and `b` are equal to each other. Return false otherwise. Uses the `equals` method on `a` if present, otherwise performs a deep comparison.
* @locus Anywhere
* @param {EJSON} a
* @param {EJSON} b
* @param {Object} [options]
* @param {Boolean} options.keyOrderSensitive Compare in key sensitive order, if supported by the JavaScript implementation. For example, `{a: 1, b: 2}` is equal to `{b: 2, a: 1}` only when `keyOrderSensitive` is `false`. The default is `false`.
*/
}, {
key: 'equals',
value: function equals(a, b, options) {
var _this = this;
var i;
var keyOrderSensitive = !!(options && options.keyOrderSensitive);
if (a === b) {
return true;
}
if (_isNaN(a) && _isNaN(b)) {
return true; // This differs from the IEEE spec for NaN equality, b/c we don't want
// anything ever with a NaN to be poisoned from becoming equal to anything.
}
if (!a || !b) {
// if either one is falsy, they'd have to be === to be equal
return false;
}
if (!((typeof a === 'undefined' ? 'undefined' : _typeof(a)) === 'object' && (typeof b === 'undefined' ? 'undefined' : _typeof(b)) === 'object')) {
return false;
}
if (a instanceof Date && b instanceof Date) {
return a.valueOf() === b.valueOf();
}
if (this.isBinary(a) && this.isBinary(b)) {
if (a.length !== b.length) {
return false;
}
for (i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
if (typeof a.equals === 'function') {
return a.equals(b, options);
}
if (typeof b.equals === 'function') {
return b.equals(a, options);
}
if (a instanceof Array) {
if (!(b instanceof Array)) {
return false;
}
if (a.length !== b.length) {
return false;
}
for (i = 0; i < a.length; i++) {
if (!this.equals(a[i], b[i], options)) {
return false;
}
}
return true;
}
// fallback for custom types that don't implement their own equals
switch (this._isCustomType(a) + this._isCustomType(b)) {
case 1:
return false;
case 2:
return this.equals(this.toJSONValue(a), this.toJSONValue(b));
}
// fall back to structural equality of objects
var ret;
if (keyOrderSensitive) {
var bKeys = (0, _keys3.default)(b);
i = 0;
ret = (0, _keys3.default)(a).every(function (x) {
if (i >= bKeys.length) {
return false;
}
if (x !== bKeys[i]) {
return false;
}
if (!_this.equals(a[x], b[bKeys[i]], options)) {
return false;
}
i++;
return true;
});
return ret && i === bKeys.length;
} else {
i = 0;
ret = (0, _keys3.default)(a).every(function (key) {
if (!_has(b, key)) {
return false;
}
if (!_this.equals(a[key], b[key], options)) {
return false;
}
i++;
return true;
});
return ret && (0, _keys3.default)(b).length === i;
}
}
/**
* @summary Return a deep copy of `val`.
* @locus Anywhere
* @param {EJSON} val A value to copy.
*/
}, {
key: 'clone',
value: function clone(v) {
var _this2 = this;
var ret;
if ((typeof v === 'undefined' ? 'undefined' : _typeof(v)) !== 'object') {
return v;
}
if (v === null) {
return null; // null has typeof 'object'
}
if (v instanceof Date) {
return new Date(v.getTime());
}
// RegExps are not really EJSON elements (eg we don't define a serialization
// for them), but they're immutable anyway, so we can support them in clone.
if (v instanceof RegExp) {
return v;
}
if (this.isBinary(v)) {
ret = _Base2.default.newBinary(v.length);
for (var i = 0; i < v.length; i++) {
ret[i] = v[i];
}
return ret;
}
if (_checkTypes2.default.array(v) || _isArguments(v)) {
ret = [];
for (var i = 0; i < v.length; i++) {
ret[i] = this.clone(v[i]);
}
return ret;
}
// handle general user-defined typed Objects if they have a clone method
if (typeof v.clone === 'function') {
return v.clone();
}
// handle other custom types
if (this._isCustomType(v)) {
return this.fromJSONValue(this.clone(this.toJSONValue(v)), true);
}
// handle other objects
ret = {};
(0, _forEach2.default)(v, function (val, key) {
ret[key] = _this2.clone(val);
});
return ret;
}
}, {
key: 'newBinary',
value: function newBinary(len) {
return _Base2.default.newBinary(len);
}
}, {
key: '_setupBuiltinConverters',
value: function _setupBuiltinConverters() {
var _this3 = this;
this._builtinConverters = [{ // Date
matchJSONValue: function matchJSONValue(obj) {
return _has(obj, '$date') && (0, _keys3.default)(obj).length === 1;
},
matchObject: function matchObject(obj) {
return obj instanceof Date;
},
toJSONValue: function toJSONValue(obj) {
return { $date: obj.getTime() };
},
fromJSONValue: function fromJSONValue(obj) {
return new Date(obj.$date);
}
}, { // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object'
// which we match.)
matchJSONValue: function matchJSONValue(obj) {
return _has(obj, '$InfNaN') && (0, _keys3.default)(obj).length === 1;
},
matchObject: _isInfOrNan,
toJSONValue: function toJSONValue(obj) {
var sign;
if (_isNaN(obj)) {
sign = 0;
} else if (obj === Infinity) {
sign = 1;
} else {
sign = -1;
}
return { $InfNaN: sign };
},
fromJSONValue: function fromJSONValue(obj) {
return obj.$InfNaN / 0;
}
}, { // Binary
matchJSONValue: function matchJSONValue(obj) {
return _has(obj, '$binary') && (0, _keys3.default)(obj).length === 1;
},
matchObject: function matchObject(obj) {
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array || obj && _has(obj, '$Uint8ArrayPolyfill');
},
toJSONValue: function toJSONValue(obj) {
return { $binary: _Base2.default.encode(obj) };
},
fromJSONValue: function fromJSONValue(obj) {
return _Base2.default.decode(obj.$binary);
}
}, { // Escaping one level
matchJSONValue: function matchJSONValue(obj) {
return _has(obj, '$escape') && (0, _keys3.default)(obj).length === 1;
},
matchObject: function matchObject(obj) {
if (!_checkTypes2.default.assigned(obj) || _checkTypes2.default.emptyObject(obj) || _checkTypes2.default.object(obj) && (0, _keys3.default)(obj).length > 2) {
return false;
}
return (0, _some3.default)(_this3._builtinConverters, function (converter) {
return converter.matchJSONValue(obj);
});
},
toJSONValue: function toJSONValue(obj) {
var newObj = {};
(0, _forEach2.default)(obj, function (val, key) {
newObj[key] = _this3.toJSONValue(val);
});
return { $escape: newObj };
},
fromJSONValue: function fromJSONValue(obj) {
var newObj = {};
(0, _forEach2.default)(obj.$escape, function (val, key) {
newObj[key] = _this3.fromJSONValue(val);
});
return newObj;
}
}, { // Custom
matchJSONValue: function matchJSONValue(obj) {
return _has(obj, '$type') && _has(obj, '$value') && (0, _keys3.default)(obj).length === 2;
},
matchObject: function matchObject(obj) {
return _this3._isCustomType(obj);
},
toJSONValue: function toJSONValue(obj) {
var jsonValue = obj.toJSONValue();
return { $type: obj.typeName(), $value: jsonValue };
},
fromJSONValue: function fromJSONValue(obj) {
var typeName = obj.$type;
if (!_has(_this3._customTypes, typeName)) {
throw new Error('Custom EJSON type ' + typeName + ' is not defined');
}
var converter = _this3._customTypes[typeName];
return converter(obj.$value);
}
}];
}
}, {
key: '_isCustomType',
value: function _isCustomType(obj) {
return obj && typeof obj.toJSONValue === 'function' && typeof obj.typeName === 'function' && _has(this._customTypes, obj.typeName());
}
/**
* For both arrays and objects, in-place modification.
*/
}, {
key: '_adjustTypesToJSONValue',
value: function _adjustTypesToJSONValue(obj) {
var _this4 = this;
// Is it an atom that we need to adjust?
if (obj === null) {
return null;
}
var maybeChanged = this._toJSONValueHelper(obj);
if (maybeChanged !== undefined) {
return maybeChanged;
}
// Other atoms are unchanged.
if ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) !== 'object') {
return obj;
}
// Iterate over array or object structure.
(0, _forEach2.default)(obj, function (value, key) {
if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) !== 'object' && value !== undefined && !_isInfOrNan(value)) {
return;
}
var changed = _this4._toJSONValueHelper(value);
if (changed) {
obj[key] = changed;
return;
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
_this4._adjustTypesToJSONValue(value);
});
return obj;
}
/**
* Either return the JSON-compatible version of the argument, or undefined
* (if the item isn't itself replaceable, but maybe some fields in it are)
*/
}, {
key: '_toJSONValueHelper',
value: function _toJSONValueHelper(item) {
for (var i = 0; i < this._builtinConverters.length; i++) {
var converter = this._builtinConverters[i];
if (converter.matchObject(item)) {
return converter.toJSONValue(item);
}
}
return undefined;
}
/**
* For both arrays and objects. Tries its best to just
* use the object you hand it, but may return something
* different if the object you hand it itself needs changing.
*/
}, {
key: '_adjustTypesFromJSONValue',
value: function _adjustTypesFromJSONValue(obj) {
var _this5 = this;
if (obj === null) {
return null;
}
var maybeChanged = this._fromJSONValueHelper(obj);
if (maybeChanged !== obj) {
return maybeChanged;
}
// Other atoms are unchanged.
if ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) !== 'object') {
return obj;
}
(0, _forEach2.default)(obj, function (value, key) {
if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') {
var changed = _this5._fromJSONValueHelper(value);
if (value !== changed) {
obj[key] = changed;
return;
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
_this5._adjustTypesFromJSONValue(value);
}
});
return obj;
}
/**
* Either return the argument changed to have the non-json
* rep of itself (the Object version) or the argument itself.
* DOES NOT RECURSE. For actually getting the fully-changed value,
* use EJSON.fromJSONValue
*/
}, {
key: '_fromJSONValueHelper',
value: function _fromJSONValueHelper(value) {
if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null) {
if ((0, _keys3.default)(value).length <= 2 && (0, _keys3.default)(value).every(function (k) {
return typeof k === 'string' && k.substr(0, 1) === '$';
})) {
for (var i = 0; i < this._builtinConverters.length; i++) {
var converter = this._builtinConverters[i];
if (converter.matchJSONValue(value)) {
return converter.fromJSONValue(value);
}
}
}
}
return value;
}
}]);
return EJSON;
}();
exports.default = new EJSON();