var util = require('./util'); var Queue = require('./Queue'); /** * DataSet * * Usage: * var dataSet = new DataSet({ * fieldId: '_id', * type: { * // ... * } * }); * * dataSet.add(item); * dataSet.add(data); * dataSet.update(item); * dataSet.update(data); * dataSet.remove(id); * dataSet.remove(ids); * var data = dataSet.get(); * var data = dataSet.get(id); * var data = dataSet.get(ids); * var data = dataSet.get(ids, options, data); * dataSet.clear(); * * A data set can: * - add/remove/update data * - gives triggers upon changes in the data * - can import/export data in various data formats * * @param {Array} [data] Optional array with initial data * @param {Object} [options] Available options: * {String} fieldId Field name of the id in the * items, 'id' by default. * {Object. listener.callback != callback); } }; // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) DataSet.prototype.unsubscribe = function () { throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.'); }; /** * Trigger an event * @param {String} event * @param {Object | null} params * @param {String} [senderId] Optional id of the sender. * @private */ DataSet.prototype._trigger = function (event, params, senderId) { if (event == '*') { throw new Error('Cannot trigger event *'); } var subscribers = []; if (event in this._subscribers) { subscribers = subscribers.concat(this._subscribers[event]); } if ('*' in this._subscribers) { subscribers = subscribers.concat(this._subscribers['*']); } for (var i = 0, len = subscribers.length; i < len; i++) { var subscriber = subscribers[i]; if (subscriber.callback) { subscriber.callback(event, params, senderId || null); } } }; /** * Add data. * Adding an item will fail when there already is an item with the same id. * @param {Object | Array} data * @param {String} [senderId] Optional sender id * @return {Array} addedIds Array with the ids of the added items */ DataSet.prototype.add = function (data, senderId) { var addedIds = [], id, me = this; if (Array.isArray(data)) { // Array for (var i = 0, len = data.length; i < len; i++) { id = me._addItem(data[i]); addedIds.push(id); } } else if (data && typeof data === 'object') { // Single item id = me._addItem(data); addedIds.push(id); } else { throw new Error('Unknown dataType'); } if (addedIds.length) { this._trigger('add', {items: addedIds}, senderId); } return addedIds; }; /** * Update existing items. When an item does not exist, it will be created * @param {Object | Array} data * @param {String} [senderId] Optional sender id * @return {Array} updatedIds The ids of the added or updated items */ DataSet.prototype.update = function (data, senderId) { var addedIds = []; var updatedIds = []; var oldData = []; var updatedData = []; var me = this; var fieldId = me._fieldId; var addOrUpdate = function (item) { var id = item[fieldId]; if (me._data[id]) { var oldItem = util.extend({}, me._data[id]); // update item id = me._updateItem(item); updatedIds.push(id); updatedData.push(item); oldData.push(oldItem); } else { // add new item id = me._addItem(item); addedIds.push(id); } }; if (Array.isArray(data)) { // Array for (var i = 0, len = data.length; i < len; i++) { if (data[i] && typeof data[i] === 'object'){ addOrUpdate(data[i]); } else { console.warn('Ignoring input item, which is not an object at index ' + i); } } } else if (data && typeof data === 'object') { // Single item addOrUpdate(data); } else { throw new Error('Unknown dataType'); } if (addedIds.length) { this._trigger('add', {items: addedIds}, senderId); } if (updatedIds.length) { var props = { items: updatedIds, oldData: oldData, data: updatedData }; // TODO: remove deprecated property 'data' some day //Object.defineProperty(props, 'data', { // 'get': (function() { // console.warn('Property data is deprecated. Use DataSet.get(ids) to retrieve the new data, use the oldData property on this object to get the old data'); // return updatedData; // }).bind(this) //}); this._trigger('update', props, senderId); } return addedIds.concat(updatedIds); }; /** * Get a data item or multiple items. * * Usage: * * get() * get(options: Object) * * get(id: Number | String) * get(id: Number | String, options: Object) * * get(ids: Number[] | String[]) * get(ids: Number[] | String[], options: Object) * * Where: * * {Number | String} id The id of an item * {Number[] | String{}} ids An array with ids of items * {Object} options An Object with options. Available options: * {String} [returnType] Type of data to be returned. * Can be 'Array' (default) or 'Object'. * {Object.} [type] * {String[]} [fields] field names to be returned * {function} [filter] filter items * {String | function} [order] Order the items by a field name or custom sort function. * @throws Error */ DataSet.prototype.get = function (args) { var me = this; // parse the arguments var id, ids, options; var firstType = util.getType(arguments[0]); if (firstType == 'String' || firstType == 'Number') { // get(id [, options]) id = arguments[0]; options = arguments[1]; } else if (firstType == 'Array') { // get(ids [, options]) ids = arguments[0]; options = arguments[1]; } else { // get([, options]) options = arguments[0]; } // determine the return type var returnType; if (options && options.returnType) { var allowedValues = ['Array', 'Object']; returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType; } else { returnType = 'Array'; } // build options var type = options && options.type || this._options.type; var filter = options && options.filter; var items = [], item, itemIds, itemId, i, len; // convert items if (id != undefined) { // return a single item item = me._getItem(id, type); if (item && filter && !filter(item)) { item = null; } } else if (ids != undefined) { // return a subset of items for (i = 0, len = ids.length; i < len; i++) { item = me._getItem(ids[i], type); if (!filter || filter(item)) { items.push(item); } } } else { // return all items itemIds = Object.keys(this._data); for (i = 0, len = itemIds.length; i < len; i++) { itemId = itemIds[i]; item = me._getItem(itemId, type); if (!filter || filter(item)) { items.push(item); } } } // order the results if (options && options.order && id == undefined) { this._sort(items, options.order); } // filter fields of the items if (options && options.fields) { var fields = options.fields; if (id != undefined) { item = this._filterFields(item, fields); } else { for (i = 0, len = items.length; i < len; i++) { items[i] = this._filterFields(items[i], fields); } } } // return the results if (returnType == 'Object') { var result = {}, resultant; for (i = 0, len = items.length; i < len; i++) { resultant = items[i]; result[resultant.id] = resultant; } return result; } else { if (id != undefined) { // a single item return item; } else { // just return our array return items; } } }; /** * Get ids of all items or from a filtered set of items. * @param {Object} [options] An Object with options. Available options: * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Array} ids */ DataSet.prototype.getIds = function (options) { var data = this._data, filter = options && options.filter, order = options && options.order, type = options && options.type || this._options.type, itemIds = Object.keys(data), i, len, id, item, items, ids = []; if (filter) { // get filtered items if (order) { // create ordered list items = []; for (i = 0, len = itemIds.length; i < len; i++) { id = itemIds[i]; item = this._getItem(id, type); if (filter(item)) { items.push(item); } } this._sort(items, order); for (i = 0, len = items.length; i < len; i++) { ids.push(items[i][this._fieldId]); } } else { // create unordered list for (i = 0, len = itemIds.length; i < len; i++) { id = itemIds[i]; item = this._getItem(id, type); if (filter(item)) { ids.push(item[this._fieldId]); } } } } else { // get all items if (order) { // create an ordered list items = []; for (i = 0, len = itemIds.length; i < len; i++) { id = itemIds[i]; items.push(data[id]); } this._sort(items, order); for (i = 0, len = items.length; i < len; i++) { ids.push(items[i][this._fieldId]); } } else { // create unordered list for (i = 0, len = itemIds.length; i < len; i++) { id = itemIds[i]; item = data[id]; ids.push(item[this._fieldId]); } } } return ids; }; /** * Returns the DataSet itself. Is overwritten for example by the DataView, * which returns the DataSet it is connected to instead. */ DataSet.prototype.getDataSet = function () { return this; }; /** * Execute a callback function for every item in the dataset. * @param {function} callback * @param {Object} [options] Available options: * {Object.} [type] * {String[]} [fields] filter fields * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. */ DataSet.prototype.forEach = function (callback, options) { var filter = options && options.filter, type = options && options.type || this._options.type, data = this._data, itemIds = Object.keys(data), i, len, item, id; if (options && options.order) { // execute forEach on ordered list var items = this.get(options); for (i = 0, len = items.length; i < len; i++) { item = items[i]; id = item[this._fieldId]; callback(item, id); } } else { // unordered for (i = 0, len = itemIds.length; i < len; i++) { id = itemIds[i]; item = this._getItem(id, type); if (!filter || filter(item)) { callback(item, id); } } } }; /** * Map every item in the dataset. * @param {function} callback * @param {Object} [options] Available options: * {Object.} [type] * {String[]} [fields] filter fields * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Object[]} mappedItems */ DataSet.prototype.map = function (callback, options) { var filter = options && options.filter, type = options && options.type || this._options.type, mappedItems = [], data = this._data, itemIds = Object.keys(data), i, len, id, item; // convert and filter items for (i = 0, len = itemIds.length; i < len; i++) { id = itemIds[i]; item = this._getItem(id, type); if (!filter || filter(item)) { mappedItems.push(callback(item, id)); } } // order items if (options && options.order) { this._sort(mappedItems, options.order); } return mappedItems; }; /** * Filter the fields of an item * @param {Object | null} item * @param {String[]} fields Field names * @return {Object | null} filteredItem or null if no item is provided * @private */ DataSet.prototype._filterFields = function (item, fields) { if (!item) { // item is null return item; } var filteredItem = {}, itemFields = Object.keys(item), len = itemFields.length, i, field; if(Array.isArray(fields)){ for (i = 0; i < len; i++) { field = itemFields[i]; if (fields.indexOf(field) != -1) { filteredItem[field] = item[field]; } } }else{ for (i = 0; i < len; i++) { field = itemFields[i]; if (fields.hasOwnProperty(field)) { filteredItem[fields[field]] = item[field]; } } } return filteredItem; }; /** * Sort the provided array with items * @param {Object[]} items * @param {String | function} order A field name or custom sort function. * @private */ DataSet.prototype._sort = function (items, order) { if (util.isString(order)) { // order by provided field name var name = order; // field name items.sort(function (a, b) { var av = a[name]; var bv = b[name]; return (av > bv) ? 1 : ((av < bv) ? -1 : 0); }); } else if (typeof order === 'function') { // order by sort function items.sort(order); } // TODO: extend order by an Object {field:String, direction:String} // where direction can be 'asc' or 'desc' else { throw new TypeError('Order must be a function or a string'); } }; /** * Remove an object by pointer or by id * @param {String | Number | Object | Array} id Object or id, or an array with * objects or ids to be removed * @param {String} [senderId] Optional sender id * @return {Array} removedIds */ DataSet.prototype.remove = function (id, senderId) { var removedIds = [], removedItems = [], ids = [], i, len, itemId, item; // force everything to be an array for simplicity ids = Array.isArray(id) ? id : [id]; for (i = 0, len = ids.length; i < len; i++) { item = this._remove(ids[i]); if (item) { itemId = item[this._fieldId]; if (itemId != undefined) { removedIds.push(itemId); removedItems.push(item); } } } if (removedIds.length) { this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId); } return removedIds; }; /** * Remove an item by its id * @param {Number | String | Object} id id or item * @returns {Number | String | null} id * @private */ DataSet.prototype._remove = function (id) { var item, ident; // confirm the id to use based on the args type if (util.isNumber(id) || util.isString(id)) { ident = id; } else if (id && typeof id === 'object') { ident = id[this._fieldId]; // look for the identifier field using _fieldId } // do the remove if the item is found if (ident !== undefined && this._data[ident]) { item = this._data[ident]; delete this._data[ident]; this.length--; return item; } return null; }; /** * Clear the data * @param {String} [senderId] Optional sender id * @return {Array} removedIds The ids of all removed items */ DataSet.prototype.clear = function (senderId) { var i, len; var ids = Object.keys(this._data); var items = []; for (i = 0, len = ids.length; i < len; i++) { items.push(this._data[ids[i]]); } this._data = {}; this.length = 0; this._trigger('remove', {items: ids, oldData: items}, senderId); return ids; }; /** * Find the item with maximum value of a specified field * @param {String} field * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.max = function (field) { var data = this._data, itemIds = Object.keys(data), max = null, maxField = null, i, len; for (i = 0, len = itemIds.length; i < len; i++) { var id = itemIds[i]; var item = data[id]; var itemField = item[field]; if (itemField != null && (!max || itemField > maxField)) { max = item; maxField = itemField; } } return max; }; /** * Find the item with minimum value of a specified field * @param {String} field * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.min = function (field) { var data = this._data, itemIds = Object.keys(data), min = null, minField = null, i, len; for (i = 0, len = itemIds.length; i < len; i++) { var id = itemIds[i]; var item = data[id]; var itemField = item[field]; if (itemField != null && (!min || itemField < minField)) { min = item; minField = itemField; } } return min; }; /** * Find all distinct values of a specified field * @param {String} field * @return {Array} values Array containing all distinct values. If data items * do not contain the specified field are ignored. * The returned array is unordered. */ DataSet.prototype.distinct = function (field) { var data = this._data; var itemIds = Object.keys(data); var values = []; var fieldType = this._options.type && this._options.type[field] || null; var count = 0; var i, j, len; for (i = 0, len = itemIds.length; i < len; i++) { var id = itemIds[i]; var item = data[id]; var value = item[field]; var exists = false; for (j = 0; j < count; j++) { if (values[j] == value) { exists = true; break; } } if (!exists && (value !== undefined)) { values[count] = value; count++; } } if (fieldType) { for (i = 0, len = values.length; i < len; i++) { values[i] = util.convert(values[i], fieldType); } } return values; }; /** * Add a single item. Will fail when an item with the same id already exists. * @param {Object} item * @return {String} id * @private */ DataSet.prototype._addItem = function (item) { var id = item[this._fieldId]; if (id != undefined) { // check whether this id is already taken if (this._data[id]) { // item already exists throw new Error('Cannot add item: item with id ' + id + ' already exists'); } } else { // generate an id id = util.randomUUID(); item[this._fieldId] = id; } var d = {}, fields = Object.keys(item), i, len; for (i = 0, len = fields.length; i < len; i++) { var field = fields[i]; var fieldType = this._type[field]; // type may be undefined d[field] = util.convert(item[field], fieldType); } this._data[id] = d; this.length++; return id; }; /** * Get an item. Fields can be converted to a specific type * @param {String} id * @param {Object.} [types] field types to convert * @return {Object | null} item * @private */ DataSet.prototype._getItem = function (id, types) { var field, value, i, len; // get the item from the dataset var raw = this._data[id]; if (!raw) { return null; } // convert the items field types var converted = {}, fields = Object.keys(raw); if (types) { for (i = 0, len = fields.length; i < len; i++) { field = fields[i]; value = raw[field]; converted[field] = util.convert(value, types[field]); } } else { // no field types specified, no converting needed for (i = 0, len = fields.length; i < len; i++) { field = fields[i]; value = raw[field]; converted[field] = value; } } if (!converted[this._fieldId]) { converted[this._fieldId] = raw.id; } return converted; }; /** * Update a single item: merge with existing item. * Will fail when the item has no id, or when there does not exist an item * with the same id. * @param {Object} item * @return {String} id * @private */ DataSet.prototype._updateItem = function (item) { var id = item[this._fieldId]; if (id == undefined) { throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); } var d = this._data[id]; if (!d) { // item doesn't exist throw new Error('Cannot update item: no item with id ' + id + ' found'); } // merge with current item var fields = Object.keys(item); for (var i = 0, len = fields.length; i < len; i++) { var field = fields[i]; var fieldType = this._type[field]; // type may be undefined d[field] = util.convert(item[field], fieldType); } return id; }; module.exports = DataSet;