marsdb
Version:
MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6
510 lines (483 loc) • 15 kB
JavaScript
/**
* Based on Meteor's EJSON package.
* Rewrite with ES6 and better formated for passing
* linter
*/
import Base64 from './Base64';
import _some from 'fast.js/array/some';
import _check from 'check-types';
import _keys from 'fast.js/object/keys';
import _each from 'fast.js/forEach';
// Internal utils
function _isNaN(val) {
return typeof val === 'number' && val != +val;
}
function _has(obj, key) {
return _check.object(obj) && obj.hasOwnProperty(key);
}
function _isInfOrNan(val) {
return _isNaN(val) || val === Infinity || val === -Infinity;
}
function _isArguments(val) {
return (
!!val && typeof val == 'object' &&
Object.prototype.hasOwnProperty.call(val, 'callee') &&
!Object.prototype.propertyIsEnumerable.call(val, 'callee')
);
}
export class EJSON {
// @ngInject
constructor() {
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.
*/
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.
*/
toJSONValue(item) {
var changed = this._toJSONValueHelper(item);
if (changed !== undefined) {
return changed;
}
if (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.
*/
fromJSONValue(item) {
var changed = this._fromJSONValueHelper(item);
if (changed === item && 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.
*/
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.
*/
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
*/
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`.
*/
equals(a, b, options) {
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 === 'object' && 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 = _keys(b);
i = 0;
ret = _keys(a).every((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 = _keys(a).every((key) => {
if (!_has(b, key)) {
return false;
}
if (!this.equals(a[key], b[key], options)) {
return false;
}
i++;
return true;
});
return ret && _keys(b).length === i;
}
}
/**
* @summary Return a deep copy of `val`.
* @locus Anywhere
* @param {EJSON} val A value to copy.
*/
clone(v) {
var ret;
if (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 = Base64.newBinary(v.length);
for (let i = 0; i < v.length; i++) {
ret[i] = v[i];
}
return ret;
}
if (_check.array(v) || _isArguments(v)) {
ret = [];
for (let 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 = {};
_each(v, (val, key) => {
ret[key] = this.clone(val);
});
return ret;
}
newBinary(len) {
return Base64.newBinary(len);
}
_setupBuiltinConverters() {
this._builtinConverters = [
{ // Date
matchJSONValue: (obj) => {
return _has(obj, '$date') &&
_keys(obj).length === 1;
},
matchObject: (obj) => {
return obj instanceof Date;
},
toJSONValue: (obj) => {
return {$date: obj.getTime()};
},
fromJSONValue: (obj) => {
return new Date(obj.$date);
},
},
{ // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object'
// which we match.)
matchJSONValue: (obj) => {
return _has(obj, '$InfNaN') &&
_keys(obj).length === 1;
},
matchObject: _isInfOrNan,
toJSONValue: (obj) => {
var sign;
if (_isNaN(obj)) {
sign = 0;
} else if (obj === Infinity) {
sign = 1;
} else {
sign = -1;
}
return {$InfNaN: sign};
},
fromJSONValue: (obj) => {
return obj.$InfNaN/0;
},
},
{ // Binary
matchJSONValue: (obj) => {
return _has(obj, '$binary') &&
_keys(obj).length === 1;
},
matchObject: (obj) => {
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array ||
(obj && _has(obj, '$Uint8ArrayPolyfill'));
},
toJSONValue: (obj) => {
return {$binary: Base64.encode(obj)};
},
fromJSONValue: (obj) => {
return Base64.decode(obj.$binary);
},
},
{ // Escaping one level
matchJSONValue: (obj) => {
return _has(obj, '$escape') &&
_keys(obj).length === 1;
},
matchObject: (obj) => {
if (
!_check.assigned(obj) || _check.emptyObject(obj) ||
(_check.object(obj) && _keys(obj).length > 2)
) {
return false;
}
return _some(this._builtinConverters, (converter) => {
return converter.matchJSONValue(obj);
});
},
toJSONValue: (obj) => {
var newObj = {};
_each(obj, (val, key) => {
newObj[key] = this.toJSONValue(val);
});
return {$escape: newObj};
},
fromJSONValue: (obj) => {
var newObj = {};
_each(obj.$escape, (val, key) => {
newObj[key] = this.fromJSONValue(val);
});
return newObj;
},
},
{ // Custom
matchJSONValue: (obj) => {
return _has(obj, '$type') &&
_has(obj, '$value') &&
_keys(obj).length === 2;
},
matchObject: (obj) => {
return this._isCustomType(obj);
},
toJSONValue: (obj) => {
var jsonValue = obj.toJSONValue();
return {$type: obj.typeName(), $value: jsonValue};
},
fromJSONValue: (obj) => {
var typeName = obj.$type;
if (!_has(this._customTypes, typeName)) {
throw new Error('Custom EJSON type ' + typeName + ' is not defined');
}
var converter = this._customTypes[typeName];
return converter(obj.$value);
},
},
];
}
_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.
*/
_adjustTypesToJSONValue(obj) {
// 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 !== 'object') {
return obj;
}
// Iterate over array or object structure.
_each(obj, (value, key) => {
if (typeof value !== 'object' && value !== undefined &&
!_isInfOrNan(value)) {
return;
}
var changed = this._toJSONValueHelper(value);
if (changed) {
obj[key] = changed;
return;
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
this._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)
*/
_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.
*/
_adjustTypesFromJSONValue(obj) {
if (obj === null) {
return null;
}
var maybeChanged = this._fromJSONValueHelper(obj);
if (maybeChanged !== obj) {
return maybeChanged;
}
// Other atoms are unchanged.
if (typeof obj !== 'object') {
return obj;
}
_each(obj, (value, key) => {
if (typeof value === 'object') {
var changed = this._fromJSONValueHelper(value);
if (value !== changed) {
obj[key] = changed;
return;
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
this._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
*/
_fromJSONValueHelper(value) {
if (typeof value === 'object' && value !== null) {
if (_keys(value).length <= 2 &&
_keys(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;
}
}
export default new EJSON();