mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
parent
549e62411e
commit
367babc81d
1 changed files with 148 additions and 136 deletions
|
@ -1,156 +1,168 @@
|
|||
import _ from 'lodash';
|
||||
import { inflector } from 'ui/indexed_array/inflector';
|
||||
|
||||
import { inflector } from './inflector';
|
||||
|
||||
const pathGetter = _(_.get).rearg(1, 0).ary(2);
|
||||
const inflectIndex = inflector('by');
|
||||
const inflectOrder = inflector('in', 'Order');
|
||||
|
||||
const CLEAR_CACHE = {};
|
||||
const OPT_NAMES = IndexedArray.OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable'];
|
||||
const OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable'];
|
||||
|
||||
/**
|
||||
* Generic extension of Array class, which will index (and reindex) the
|
||||
* objects it contains based on their properties.
|
||||
* Generic extension of Array class, which will index (and reindex) the
|
||||
* objects it contains based on their properties.
|
||||
*
|
||||
* @class IndexedArray
|
||||
* @module utils
|
||||
* @constructor
|
||||
* @param {object} [config] - describes the properties of this registry object
|
||||
* @param {string[]} [config.index] - a list of props/paths that should be used to index the docs.
|
||||
* @param {string[]} [config.group] - a list of keys/paths to group docs by.
|
||||
* @param {string[]} [config.order] - a list of keys/paths to order the keys by.
|
||||
* @param {object[]} [config.initialSet] - the initial dataset the IndexedArray should contain.
|
||||
* @param {boolean} [config.immutable] - a flag that hints to people reading the implementation
|
||||
* that this IndexedArray should not be modified. It's modification
|
||||
* methods are also removed
|
||||
* @param {Object} config describes the properties of this registry object
|
||||
* @param {Array<string>} [config.index] a list of props/paths that should be used to index the docs.
|
||||
* @param {Array<string>} [config.group] a list of keys/paths to group docs by.
|
||||
* @param {Array<string>} [config.order] a list of keys/paths to order the keys by.
|
||||
* @param {Array<any>} [config.initialSet] the initial dataset the IndexedArray should contain.
|
||||
* @param {boolean} [config.immutable] a flag that hints to people reading the implementation that this IndexedArray
|
||||
* should not be modified
|
||||
*/
|
||||
_.class(IndexedArray).inherits(Array);
|
||||
export function IndexedArray(config) {
|
||||
IndexedArray.Super.call(this);
|
||||
|
||||
// just to remind future us that this list is important
|
||||
config = _.pick(config || {}, OPT_NAMES);
|
||||
export class IndexedArray {
|
||||
static OPT_NAMES = OPT_NAMES
|
||||
|
||||
this.raw = [];
|
||||
constructor(config) {
|
||||
config = _.pick(config || {}, OPT_NAMES);
|
||||
|
||||
// setup indices
|
||||
this._indexNames = _.union(
|
||||
this._setupIndices(config.group, inflectIndex, _.organizeBy),
|
||||
this._setupIndices(config.index, inflectIndex, _.indexBy),
|
||||
this._setupIndices(config.order, inflectOrder, _.sortBy)
|
||||
);
|
||||
// use defineProperty so that value can't be changed
|
||||
Object.defineProperty(this, 'raw', { value: [] });
|
||||
|
||||
if (config.initialSet) {
|
||||
this.push.apply(this, config.initialSet);
|
||||
this._indexNames = _.union(
|
||||
this._setupIndex(config.group, inflectIndex, _.organizeBy),
|
||||
this._setupIndex(config.index, inflectIndex, _.indexBy),
|
||||
this._setupIndex(config.order, inflectOrder, _.sortBy)
|
||||
);
|
||||
|
||||
if (config.initialSet) {
|
||||
this.push.apply(this, config.initialSet);
|
||||
}
|
||||
|
||||
Object.defineProperty(this, 'immutable', { value: !!config.immutable });
|
||||
}
|
||||
|
||||
if (config.immutable) {
|
||||
// just a hint, bugs caused by updates not propogating would be very
|
||||
// very very hard to track down
|
||||
this.push = this.splice = undefined;
|
||||
/**
|
||||
* Remove items from this based on a predicate
|
||||
* @param {Function|Object|string} predicate - the predicate used to decide what is removed
|
||||
* @return {array} - the removed data
|
||||
*/
|
||||
remove(predicate) {
|
||||
this._assertMutable('remove');
|
||||
const out = _.remove(this, predicate);
|
||||
_.remove(this.raw, predicate);
|
||||
this._clearIndices();
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* provide a hook for the JSON serializer
|
||||
* @return {array} - a plain, vanilla array with our same data
|
||||
*/
|
||||
toJSON() {
|
||||
return this.raw;
|
||||
}
|
||||
|
||||
// wrappers for mutable Array methods
|
||||
copyWithin(...args) { return this._mutation('copyWithin', args); }
|
||||
fill(...args) { return this._mutation('fill', args); }
|
||||
pop(...args) { return this._mutation('pop', args); }
|
||||
push(...args) { return this._mutation('push', args); }
|
||||
reverse(...args) { return this._mutation('reverse', args); }
|
||||
shift(...args) { return this._mutation('shift', args); }
|
||||
sort(...args) { return this._mutation('sort', args); }
|
||||
splice(...args) { return this._mutation('splice', args); }
|
||||
unshift(...args) { return this._mutation('unshift', args); }
|
||||
|
||||
/**
|
||||
* If this instance of IndexedArray is not mutable, throw an error
|
||||
* @private
|
||||
* @param {String} methodName - user facing method name, for error message
|
||||
* @return {undefined}
|
||||
*/
|
||||
_assertMutable(methodName) {
|
||||
if (this.immutable) {
|
||||
throw new Error(`${methodName}() is not allowed on immutable IndexedArray instances`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute some mutable method from the Array prototype
|
||||
* on the IndexedArray and this.raw
|
||||
*
|
||||
* @private
|
||||
* @param {string} methodName
|
||||
* @param {Array<any>} args
|
||||
* @return {any}
|
||||
*/
|
||||
_mutation(methodName, args) {
|
||||
this._assertMutable(methodName);
|
||||
super[methodName].apply(this, args);
|
||||
this._clearIndices();
|
||||
return super[methodName].apply(this.raw, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create indices for a group of object properties. getters and setters are used to
|
||||
* read and control the indices.
|
||||
* @private
|
||||
* @param {string[]} props - the properties that should be used to index docs
|
||||
* @param {function} inflect - a function that will be called with a property name, and
|
||||
* creates the public property at which the index will be exposed
|
||||
* @param {function} op - the function that will be used to create the indices, it is passed
|
||||
* the raw representaion of the registry, and a getter for reading the
|
||||
* right prop
|
||||
*
|
||||
* @returns {string[]} - the public keys of all indices created
|
||||
*/
|
||||
_setupIndex(props, inflect, op) {
|
||||
// shortcut for empty props
|
||||
if (!props || props.length === 0) return;
|
||||
|
||||
return props.map(prop => {
|
||||
const indexName = inflect(prop);
|
||||
const getIndexValueFromItem = pathGetter.partial(prop).value();
|
||||
let cache;
|
||||
|
||||
Object.defineProperty(this, indexName, {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
|
||||
set: val => {
|
||||
// can't set any value other than the CLEAR_CACHE constant
|
||||
if (val === CLEAR_CACHE) {
|
||||
cache = false;
|
||||
} else {
|
||||
throw new TypeError(indexName + ' can not be set, it is a computed index of values');
|
||||
}
|
||||
},
|
||||
get: () => {
|
||||
if (!cache) {
|
||||
cache = op(this.raw, getIndexValueFromItem);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
});
|
||||
|
||||
return indexName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached index/group/order caches so they will be recreated
|
||||
* on next access
|
||||
* @private
|
||||
* @return {undefined}
|
||||
*/
|
||||
_clearIndices() {
|
||||
this._indexNames.forEach(name => {
|
||||
this[name] = CLEAR_CACHE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create indices for a group of object properties. getters and setters are used to
|
||||
* read and control the indices.
|
||||
*
|
||||
* @param {string[]} props - the properties that should be used to index docs
|
||||
* @param {function} inflect - a function that will be called with a property name, and
|
||||
* creates the public property at which the index will be exposed
|
||||
* @param {function} op - the function that will be used to create the indices, it is passed
|
||||
* the raw representaion of the registry, and a getter for reading the
|
||||
* right prop
|
||||
*
|
||||
* @returns {string[]} - the public keys of all indices created
|
||||
*/
|
||||
IndexedArray.prototype._setupIndices = function (props, inflect, op) {
|
||||
// shortcut for empty props
|
||||
if (!props || props.length === 0) return;
|
||||
|
||||
const self = this;
|
||||
return props.map(function (prop) {
|
||||
|
||||
const from = pathGetter.partial(prop).value();
|
||||
const to = inflect(prop);
|
||||
let cache;
|
||||
|
||||
Object.defineProperty(self, to, {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
|
||||
set: function (val) {
|
||||
// can't set any value other than the CLEAR_CACHE constant
|
||||
if (val === CLEAR_CACHE) {
|
||||
cache = false;
|
||||
} else {
|
||||
throw new TypeError(to + ' can not be set, it is a computed index of values');
|
||||
}
|
||||
},
|
||||
get: function () {
|
||||
return cache || (cache = op(self.raw, from));
|
||||
}
|
||||
});
|
||||
|
||||
return to;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* (Re)run index/group/order procedures to create indices of
|
||||
* sub-objects.
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
IndexedArray.prototype._clearIndices = function () {
|
||||
const self = this;
|
||||
self._indexNames.forEach(function (name) {
|
||||
self[name] = CLEAR_CACHE;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy all array methods which have side-effects, and wrap them
|
||||
* in a function that will reindex after each call, as well
|
||||
* as duplex the operation to the .raw version of the IndexedArray.
|
||||
*
|
||||
* @param {[type]} method [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
'pop push shift splice unshift reverse'.split(' ').forEach(function (method) {
|
||||
const orig = Array.prototype[method];
|
||||
|
||||
IndexedArray.prototype[method] = function (/* args... */) {
|
||||
// call the original method with this context
|
||||
orig.apply(this, arguments);
|
||||
|
||||
// run the indexers
|
||||
this._clearIndices();
|
||||
|
||||
// call the original method on our "raw" array, and return the result(s)
|
||||
return orig.apply(this.raw, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove items from this based on a predicate
|
||||
* @param {function|object|string} predicate - the predicate used to decide what is removed
|
||||
* @param {object} context - this binding for predicate
|
||||
* @return {array} - the removed data
|
||||
*/
|
||||
IndexedArray.prototype.remove = function (predicate, context) {
|
||||
const out = _.remove(this, predicate, context);
|
||||
_.remove(this.raw, predicate, context);
|
||||
this._clearIndices();
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* provide a hook for the JSON serializer
|
||||
* @return {array} - a plain, vanilla array with our same data
|
||||
*/
|
||||
IndexedArray.prototype.toJSON = function () {
|
||||
return this.raw;
|
||||
};
|
||||
// using traditional `extends Array` syntax doesn't work with babel
|
||||
// See https://babeljs.io/docs/usage/caveats/
|
||||
Object.setPrototypeOf(IndexedArray.prototype, Array.prototype);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue