jsonstore-js
Version:
A javascript JSON data store with manifold abilities of data processing
442 lines (435 loc) • 16.4 kB
JavaScript
'use strict';
/**
* options:
* store
* copyStore
* cacheKeys
* localStorage
* **/
var utils = require('./utils');
var array = require('./array');
var object = require('./object');
var patchMethods = require('./patch');
var PathListener = require('./PathListener');
var JSON_STORE_CACHE_KEY_PREFIX = 'JSON_STORE_CACHE_KEY_PREFIX';
var emptyFunc = function emptyFunc() {};
function JSONDataStore(options) {
options = options || {};
this.initialOptions = utils.copy(options);
var store = options.store,
copyStore = options.copyStore !== false;
this.copyStore = copyStore;
this.store = copyStore ? utils.copy(store) : store;
this.cacheKeys = this._getStoreKeysMap(options.cacheKeys, this.store);
this.flashKeys = this._getStoreKeysMap(options.flashKeys, this.store);
this.cacheKeyPrefix = options.cacheKeyPrefix || JSON_STORE_CACHE_KEY_PREFIX;
this.localStorage = options.localStorage;
// 'do' about attributes
this.patches = [];
this.relativePatches = [];
this.backPatches = [];
this.currentPath = [];
this.isDoing = false;
this.pathListener = new PathListener({ store: this.store, copyStore: copyStore, flashKeys: this.flashKeys });
this.initialMutationActionPath = [];
}
JSONDataStore.prototype = {
_storeUpdated: function _storeUpdated() {
this._updateCache(this.initialMutationActionPath[0]);
this.pathListener.checkPath(this.initialMutationActionPath);
},
_getStoreKeysMap: function _getStoreKeysMap(keys, store) {
var keysMap = {};
if (utils.type(keys) === 'array') {
keys.forEach(function (key) {
if (Object.hasOwnProperty.call(store, key)) {
keysMap[key] = true;
}
});
}
return keysMap;
},
_getRef: function _getRef(path) {
var ref = this.store,
i = 0,
len = path.length;
for (; i < len; i++) {
ref = ref[path[i]];
}
return ref;
},
_detectPath: function _detectPath(path) {
var detected = [],
ref = this.store,
i = 0,
len = path.length,
key = void 0,
keyType = void 0,
refType = void 0;
for (; i < len; i++) {
key = path[i];
keyType = utils.type(key);
refType = utils.type(ref);
if (refType === 'object') {
if (object.hasOwnProperty.call(key, '__value')) {
var objKey = object.getObjectKeyByValue(ref, key.__value);
if (objKey) {
ref = ref[objKey];
detected.push(objKey);
} else {
return [];
}
} else if (object.hasOwnProperty.call(ref, key)) {
ref = ref[key];
detected.push(key);
} else {
return [];
}
} else if (refType === 'array') {
if (object.hasOwnProperty.call(key, '__value')) {
var index = array.getArrayIndexByValue(ref, key.__value);
if (index > -1) {
ref = ref[index];
detected.push(index);
} else {
return [];
}
} else if (object.hasOwnProperty.call(ref, key)) {
ref = ref[key];
detected.push(key);
} else {
return [];
}
}
}
return detected;
},
_formatPath: function _formatPath(path, detect) {
var pathType = utils.type(path);
if (pathType === 'undefined' || pathType === 'null') {
path = [];
} else if (pathType !== 'array') {
path = [path];
}
if (detect !== false) {
var detected = this._detectPath(path);
if (detected.length === path.length) {
return detected;
}
return null;
}
return path.slice();
},
_moveArrayItem: function _moveArrayItem(path, moveUp) {
var fullPath = this._getFullPath(path);
if (!fullPath || fullPath.length < 1) return this;
var itemIndex = fullPath.pop(),
arr = this._getRef(fullPath);
if (utils.type(arr) !== 'array') return this;
var method = moveUp === true ? 'createMoveUp' : 'createMoveDown',
reverseMethod = method === 'createMoveUp' ? 'createMoveDown' : 'createMoveUp';
if (this.isDoing) {
this.patches.push(patchMethods[method](fullPath.concat(itemIndex)));
this.relativePatches.push(patchMethods[method](this._getRelativePath(fullPath.concat(itemIndex))));
if (moveUp === true && itemIndex > 0 || moveUp !== true && itemIndex < arr.length - 1) {
this.backPatches.unshift(patchMethods[reverseMethod](fullPath.concat(moveUp === true ? itemIndex - 1 : itemIndex + 1)));
}
}
if (moveUp === true) {
array.moveArrayItemUp(arr, itemIndex);
} else {
array.moveArrayItemDown(arr, itemIndex);
}
this._storeUpdated();
return this;
},
_getFullPath: function _getFullPath(path) {
if (utils.isReferenceType(path) && path.isFull) {
return path;
}
var currentPath = this._formatPath(this.currentPath, false),
fullPath = currentPath.concat(this._formatPath(path, false)),
formattedFullPath = this._formatPath(fullPath);
if (formattedFullPath) {
formattedFullPath.isFull = true;
}
return formattedFullPath;
},
_getRelativePath: function _getRelativePath(fullPath) {
return fullPath.slice(this.currentPath.length);
},
_composeCacheKey: function _composeCacheKey(key) {
return this.cacheKeyPrefix + '@' + key;
},
_updateCache: function _updateCache(key) {
if (this.cacheKeys[key] && this.localStorage && typeof this.localStorage.setItem === 'function') {
this.localStorage.setItem(this._composeCacheKey(key), this.get(key));
}
},
registerPathListener: function registerPathListener(path, callback, group, check) {
path = Array.isArray(path) ? path : [path];
this.pathListener.registerListener(path, callback, group, check);
},
removeListenerByPath: function removeListenerByPath(path, callback) {
path = Array.isArray(path) ? path : [path];
this.pathListener.removeListenerByPath(path, callback);
},
removeListenerByGroup: function removeListenerByGroup(group) {
this.pathListener.removeListenerByGroup(group);
},
removeAllListeners: function removeAllListeners() {
this.pathListener.removeAllListeners();
},
loadCache: function loadCache(success, error) {
var _this = this;
error = typeof error === 'function' ? error : emptyFunc;
if (this.localStorage && typeof this.localStorage.multiGet === 'function') {
var cacheKeys = this.initialOptions.cacheKeys || [];
var composedKeys = cacheKeys.map(function (key) {
return _this._composeCacheKey(key);
});
this.localStorage.multiGet(composedKeys, function (cache) {
var parsedCache = {};
composedKeys.forEach(function (composedKey, index) {
var key = cacheKeys[index];
var cachedValue = cache[composedKey];
_this.set(key, cachedValue === null ? _this.get(key) : cachedValue);
parsedCache[key] = cache[composedKey];
});
success(parsedCache);
}, error);
} else {
error('localStorage is undefined');
}
},
reInit: function reInit(options) {
JSONDataStore.call(this, options || this.initialOptions);
return this;
},
goTo: function goTo(path, addUp) {
if (!this.isDoing) {
throw new Error('You are using store.goTo outside store.do!');
}
if (addUp === true) {
this.currentPath = this._getFullPath(path);
} else {
this.currentPath = this._formatPath(path);
}
return this;
},
do: function _do(name, action, a, b, c, d, e, f) {
var result = {};
this.isDoing = true;
if (typeof name === 'function') {
name(this, action, a, b, c, d, e, f);
} else if (typeof action === 'function') {
action(this, a, b, c, d, e, f);
} else {
throw new Error('Invalid parameter action.');
}
// compose result
result.patches = this.patches;
result.relativePatches = this.relativePatches;
result.backPatches = this.backPatches;
// reset 'do' about attributes
this.patches = [];
this.relativePatches = [];
this.backPatches = [];
this.currentPath = [];
this.isDoing = false;
return result;
},
add: function add(path, value, key, parentPath) {
this.initialMutationActionPath = parentPath !== undefined ? parentPath : this._formatPath(path, false);
var ref = void 0,
refType = void 0;
path = this._getFullPath(path);
if (!path || !utils.isReferenceType(ref = this._getRef(path)) || (refType = utils.type(ref)) === 'object' && !utils.isCommonKeyType(key)) {
return this;
}
if (this.isDoing) {
this.patches.push(patchMethods.createAdd(path, value, key));
this.relativePatches.push(patchMethods.createAdd(this._getRelativePath(path), value, key));
if (refType === 'object') {
this.backPatches.unshift(patchMethods.createRemove(path.concat(key)));
} else {
this.backPatches.unshift(patchMethods.createUpdate(path, this.get(path), true));
}
}
if (refType === 'object') {
ref[key] = value;
} else {
var index = array.parseArrayIndex(key);
if (index !== undefined) {
ref.splice(index, 0, value);
} else {
ref.push(value);
}
}
this._storeUpdated();
return this;
},
remove: function remove(path, parentPath) {
this.initialMutationActionPath = parentPath !== undefined ? parentPath : this._formatPath(path, false);
if (!(path = this._getFullPath(path))) return this;
if (this.isDoing) {
this.patches.push(patchMethods.createRemove(path));
this.relativePatches.push(patchMethods.createRemove(this._getRelativePath(path)));
this.backPatches.unshift(patchMethods.createUpdate(path, this.get(path), true));
}
if (path.length < 1) {
this.store = undefined;
return this;
}
var lastKey = path.pop(),
ref = this._getRef(path),
refType = utils.type(ref);
if (refType === 'array') {
ref.splice(lastKey, 1);
} else if (refType === 'object') {
delete ref[lastKey];
}
this._storeUpdated();
return this;
},
update: function update(path, value, forceUpdate, parentPath) {
this.initialMutationActionPath = parentPath !== undefined ? parentPath : this._formatPath(path, false);
path = this._formatPath(path, false);
var lastKey = void 0,
fullPath = this._getFullPath(path);
if (fullPath) {
if (this.isDoing) {
this.patches.push(patchMethods.createUpdate(fullPath, value));
this.relativePatches.push(patchMethods.createUpdate(this._getRelativePath(fullPath), value));
this.backPatches.unshift(patchMethods.createUpdate(fullPath, this.get(fullPath)));
}
lastKey = fullPath.pop();
if (lastKey !== undefined) {
this._getRef(fullPath)[lastKey] = value;
} else {
this.store = value;
}
this._storeUpdated();
return this;
} else if (forceUpdate === true && path.length > 0) {
lastKey = path.pop();
return this.add(path, value, lastKey, this.initialMutationActionPath);
}
return this;
},
set: function set(path, value) {
return this.update(path, value, true, this._formatPath(path, false));
},
moveUp: function moveUp(path) {
this.initialMutationActionPath = this._formatPath(path, false);
return this._moveArrayItem(path, true);
},
moveDown: function moveDown(path) {
this.initialMutationActionPath = this._formatPath(path, false);
return this._moveArrayItem(path);
},
moveTo: function moveTo(from, to, key) {
var parentFromPath = this._formatPath(from, false),
parentToPath = this._formatPath(to, false);
from = this._getFullPath(from);
to = this._getFullPath(to);
if (!from || !to || !utils.isReferenceType(this._getRef(to))) return this;
this.add(to, this._getRef(from), key, parentToPath);
this.remove(from, parentFromPath);
return this;
},
exchange: function exchange(from, to) {
var parentFromPath = this._formatPath(from, false),
parentToPath = this._formatPath(to, false);
from = this._getFullPath(from);
to = this._getFullPath(to);
if (from && to) {
var fromRef = this._getRef(from),
toRef = this.get(to);
this.update(from, toRef, false, parentFromPath);
this.update(to, fromRef, false, parentToPath);
}
return this;
},
extendObject: function extendObject(path, a, b, c, d, e, f) {
this.initialMutationActionPath = this._formatPath(path, false);
var ref = void 0;
if (!(path = this._getFullPath(path)) || utils.type(ref = this._getRef(path)) !== 'object') return this;
if (this.isDoing) {
this.patches.push(patchMethods.createExtendObject.apply(this, arguments));
this.relativePatches.push(patchMethods.createExtendObject(this._getRelativePath(path), a, b, c, d, e, f));
this.backPatches.push(patchMethods.createUpdate(path, this.get(path)));
}
object.extend(ref, a, b, c, d, e, f);
this._storeUpdated();
return this;
},
spreadArray: function spreadArray(path, begin, infilling, simpleInfilling, count) {
this.initialMutationActionPath = this._formatPath(path, false);
var ref = void 0;
if (!(path = this._getFullPath(path)) || utils.type(ref = this._getRef(path)) !== 'array') {
return this;
}
begin = typeof begin === 'number' ? begin : ref.length;
if (!(utils.type(begin) === 'number')) return this;
if (this.isDoing) {
this.patches.push(patchMethods.createSpreadArray(path, begin, infilling, simpleInfilling, count));
this.relativePatches.push(patchMethods.createSpreadArray(this._getRelativePath(path), begin, infilling, simpleInfilling, count));
this.backPatches.unshift(patchMethods.createUpdate(path, this.get(path)));
}
array.spreadArray(ref, begin, infilling, simpleInfilling, count);
this._storeUpdated();
return this;
},
spread2dArrayRow: function spread2dArrayRow(path, begin, rows, simpleInfilling, count) {
this.initialMutationActionPath = this._formatPath(path, false);
var ref = void 0;
if (!(path = this._getFullPath(path)) || !array.is2dArray(ref = this._getRef(path)) || !(utils.type(begin) === 'number')) {
return this;
}
begin = typeof begin === 'number' ? begin : ref.length;
if (!(utils.type(begin) === 'number')) return this;
if (this.isDoing) {
this.patches.push(patchMethods.createSpread2dArrayRow(path, begin, rows, simpleInfilling, count));
this.relativePatches.push(patchMethods.createSpread2dArrayRow(this._getRelativePath(path), begin, rows, simpleInfilling, count));
this.backPatches.unshift(patchMethods.createUpdate(path, this.get(path)));
}
array.spread2dArrayRow(ref, begin, rows, simpleInfilling, count);
this._storeUpdated();
return this;
},
spread2dArrayCol: function spread2dArrayCol(path, begin, cols, simpleInfilling, count) {
this.initialMutationActionPath = this._formatPath(path, false);
var ref = void 0;
if (!(path = this._getFullPath(path)) || !array.is2dArray(ref = this._getRef(path)) || !(utils.type(begin) === 'number')) {
return this;
}
begin = typeof begin === 'number' ? begin : ref[0].length;
if (!(utils.type(begin) === 'number')) return this;
if (this.isDoing) {
this.patches.push(patchMethods.createSpread2dArrayCol(path, begin, cols, simpleInfilling, count));
this.relativePatches.push(patchMethods.createSpread2dArrayCol(this._getRelativePath(path), begin, cols, simpleInfilling, count));
this.backPatches.unshift(patchMethods.createUpdate(path, this.get(path)));
}
array.spread2dArrayCol(ref, begin, cols, simpleInfilling, count);
this._storeUpdated();
return this;
},
get: function get(path) {
if (path = this._getFullPath(path)) {
return this.copyStore ? utils.copy(this._getRef(path)) : this._getRef(path);
}
},
patch: function patch() {
throw new Error('This method is deprecated, use JSONStore.patch instead.');
},
applyPatch: function applyPatch(patches) {
patches = utils.type(patches) === 'array' ? patches : [patches];
patches.forEach(function (patch) {
this[patch.type].apply(this, patch.args);
}.bind(this));
return this;
}
};
JSONDataStore.Patch = patchMethods;
module.exports = JSONDataStore;