|
|
- var util = require('./util');
-
- /**
- * DataSet
- *
- * Usage:
- * var dataSet = new DataSet({
- * fieldId: '_id',
- * fieldTypes: {
- * // ...
- * }
- * });
- *
- * 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 {Object} [options] Available options:
- * {String} fieldId Field name of the id in the
- * items, 'id' by default.
- * {Object.<String, String} fieldTypes
- * A map with field names as key,
- * and the field type as value.
- */
- function DataSet (options) {
- var me = this;
-
- this.options = options || {};
- this.data = {}; // map with data indexed by id
- this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
- this.fieldTypes = {}; // field types by field name
-
- if (this.options.fieldTypes) {
- util.forEach(this.options.fieldTypes, function (value, field) {
- if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
- me.fieldTypes[field] = 'Date';
- }
- else {
- me.fieldTypes[field] = value;
- }
- });
- }
-
- // event subscribers
- this.subscribers = {};
-
- this.internalIds = {}; // internally generated id's
- }
-
- /**
- * Subscribe to an event, add an event listener
- * @param {String} event Event name. Available events: 'put', 'update',
- * 'remove'
- * @param {function} callback Callback method. Called with three parameters:
- * {String} event
- * {Object | null} params
- * {String} senderId
- * @param {String} [id] Optional id for the sender, used to filter
- * events triggered by the sender itself.
- */
- DataSet.prototype.subscribe = function (event, callback, id) {
- var subscribers = this.subscribers[event];
- if (!subscribers) {
- subscribers = [];
- this.subscribers[event] = subscribers;
- }
-
- subscribers.push({
- id: id ? String(id) : null,
- callback: callback
- });
- };
-
- /**
- * Unsubscribe from an event, remove an event listener
- * @param {String} event
- * @param {function} callback
- */
- DataSet.prototype.unsubscribe = function (event, callback) {
- var subscribers = this.subscribers[event];
- if (subscribers) {
- this.subscribers[event] = subscribers.filter(function (listener) {
- return (listener.callback != callback);
- });
- }
- };
-
- /**
- * Trigger an event
- * @param {String} event
- * @param {Object | null} params
- * @param {String} [senderId] Optional id of the sender. The event will
- * be triggered for all subscribers except the
- * sender itself.
- * @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['*']);
- }
-
- subscribers.forEach(function (listener) {
- if (listener.id != senderId && listener.callback) {
- listener.callback(event, params, senderId || null);
- }
- });
- };
-
- /**
- * Add data. Existing items with the same id will be overwritten.
- * @param {Object | Array | DataTable} data
- * @param {String} [senderId] Optional sender id, used to trigger events for
- * all but this sender's event subscribers.
- */
- DataSet.prototype.add = function (data, senderId) {
- var items = [],
- id,
- me = this;
-
- if (data instanceof Array) {
- // Array
- data.forEach(function (item) {
- var id = me._addItem(item);
- items.push(id);
- });
- }
- else if (util.isDataTable(data)) {
- // Google DataTable
- var columns = this._getColumnNames(data);
- for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
- var item = {};
- columns.forEach(function (field, col) {
- item[field] = data.getValue(row, col);
- });
- id = me._addItem(item);
- items.push(id);
- }
- }
- else if (data instanceof Object) {
- // Single item
- id = me._addItem(data);
- items.push(id);
- }
- else {
- throw new Error('Unknown dataType');
- }
-
- this._trigger('add', {items: items}, senderId);
- };
-
- /**
- * Update existing items. Items with the same id will be merged
- * @param {Object | Array | DataTable} data
- * @param {String} [senderId] Optional sender id, used to trigger events for
- * all but this sender's event subscribers.
- */
- DataSet.prototype.update = function (data, senderId) {
- var items = [],
- id,
- me = this;
-
- if (data instanceof Array) {
- // Array
- data.forEach(function (item) {
- var id = me._updateItem(item);
- items.push(id);
- });
- }
- else if (util.isDataTable(data)) {
- // Google DataTable
- var columns = this._getColumnNames(data);
- for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
- var item = {};
- columns.forEach(function (field, col) {
- item[field] = data.getValue(row, col);
- });
- id = me._updateItem(item);
- items.push(id);
- }
- }
- else if (data instanceof Object) {
- // Single item
- id = me._updateItem(data);
- items.push(id);
- }
- else {
- throw new Error('Unknown dataType');
- }
-
- this._trigger('update', {items: items}, senderId);
- };
-
- /**
- * Get a data item or multiple items
- * @param {String | Number | Array | Object} [ids] Id of a single item, or an
- * array with multiple id's, or
- * undefined or an Object with options
- * to retrieve all data.
- * @param {Object} [options] Available options:
- * {String} [type]
- * 'DataTable' or 'Array' (default)
- * {Object.<String, String>} [fieldTypes]
- * {String[]} [fields] filter fields
- * @param {Array | DataTable} [data] If provided, items will be appended
- * to this array or table. Required
- * in case of Google DataTable
- * @return {Array | Object | DataTable | null} data
- * @throws Error
- */
- DataSet.prototype.get = function (ids, options, data) {
- var me = this;
-
- // shift arguments when first argument contains the options
- if (util.getType(ids) == 'Object') {
- data = options;
- options = ids;
- ids = undefined;
- }
-
- // merge field types
- var fieldTypes = {};
- if (this.options && this.options.fieldTypes) {
- util.forEach(this.options.fieldTypes, function (value, field) {
- fieldTypes[field] = value;
- });
- }
- if (options && options.fieldTypes) {
- util.forEach(options.fieldTypes, function (value, field) {
- fieldTypes[field] = value;
- });
- }
-
- var fields = options ? options.fields : undefined;
-
- // determine the return type
- var type;
- if (options && options.type) {
- type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
-
- if (data && (type != util.getType(data))) {
- throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
- 'does not correspond with specified options.type (' + options.type + ')');
- }
- if (type == 'DataTable' && !util.isDataTable(data)) {
- throw new Error('Parameter "data" must be a DataTable ' +
- 'when options.type is "DataTable"');
- }
- }
- else if (data) {
- type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
- }
- else {
- type = 'Array';
- }
-
- if (type == 'DataTable') {
- // return a Google DataTable
- var columns = this._getColumnNames(data);
- if (ids == undefined) {
- // return all data
- util.forEach(this.data, function (item) {
- me._appendRow(data, columns, me._castItem(item));
- });
- }
- else if (util.isNumber(ids) || util.isString(ids)) {
- var item = me._castItem(me.data[ids], fieldTypes, fields);
- this._appendRow(data, columns, item);
- }
- else if (ids instanceof Array) {
- ids.forEach(function (id) {
- var item = me._castItem(me.data[id], fieldTypes, fields);
- me._appendRow(data, columns, item);
- });
- }
- else {
- throw new TypeError('Parameter "ids" must be ' +
- 'undefined, a String, Number, or Array');
- }
- }
- else {
- // return an array
- data = data || [];
- if (ids == undefined) {
- // return all data
- util.forEach(this.data, function (item) {
- data.push(me._castItem(item, fieldTypes, fields));
- });
- }
- else if (util.isNumber(ids) || util.isString(ids)) {
- // return a single item
- return this._castItem(me.data[ids], fieldTypes, fields);
- }
- else if (ids instanceof Array) {
- ids.forEach(function (id) {
- data.push(me._castItem(me.data[id], fieldTypes, fields));
- });
- }
- else {
- throw new TypeError('Parameter "ids" must be ' +
- 'undefined, a String, Number, or Array');
- }
- }
-
- return data;
- };
-
- /**
- * 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, used to trigger events for
- * all but this sender's event subscribers.
- */
- DataSet.prototype.remove = function (id, senderId) {
- var items = [],
- me = this;
-
- if (util.isNumber(id) || util.isString(id)) {
- delete this.data[id];
- delete this.internalIds[id];
- items.push(id);
- }
- else if (id instanceof Array) {
- id.forEach(function (id) {
- me.remove(id);
- });
- items = items.concat(id);
- }
- else if (id instanceof Object) {
- // search for the object
- for (var i in this.data) {
- if (this.data.hasOwnProperty(i)) {
- if (this.data[i] == id) {
- delete this.data[i];
- delete this.internalIds[i];
- items.push(i);
- }
- }
- }
- }
-
- this._trigger('remove', {items: items}, senderId);
- };
-
- /**
- * Clear the data
- * @param {String} [senderId] Optional sender id, used to trigger events for
- * all but this sender's event subscribers.
- */
- DataSet.prototype.clear = function (senderId) {
- var ids = Object.keys(this.data);
-
- this.data = {};
- this.internalIds = {};
-
- this._trigger('remove', {items: ids}, senderId);
- };
-
- /**
- * Find the item with maximum value of a specified field
- * @param {String} field
- * @return {Object} item Item containing max value, or null if no items
- */
- DataSet.prototype.max = function (field) {
- var data = this.data,
- ids = Object.keys(data);
-
- var max = null;
- var maxField = null;
- ids.forEach(function (id) {
- 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
- */
- DataSet.prototype.min = function (field) {
- var data = this.data,
- ids = Object.keys(data);
-
- var min = null;
- var minField = null;
- ids.forEach(function (id) {
- var item = data[id];
- var itemField = item[field];
- if (itemField != null && (!min || itemField < minField)) {
- min = item;
- minField = itemField;
- }
- });
-
- return min;
- };
-
- /**
- * Add a single item
- * @param {Object} item
- * @return {String} id
- * @private
- */
- DataSet.prototype._addItem = function (item) {
- var id = item[this.fieldId];
- if (id == undefined) {
- // generate an id
- id = util.randomUUID();
- item[this.fieldId] = id;
-
- this.internalIds[id] = item;
- }
-
- var d = {};
- for (var field in item) {
- if (item.hasOwnProperty(field)) {
- var type = this.fieldTypes[field]; // type may be undefined
- d[field] = util.cast(item[field], type);
- }
- }
- this.data[id] = d;
- //TODO: fail when an item with this id already exists?
-
- return id;
- };
-
- /**
- * Cast and filter the fields of an item
- * @param {Object | undefined} item
- * @param {Object.<String, String>} [fieldTypes]
- * @param {String[]} [fields]
- * @return {Object | null} castedItem
- * @private
- */
- DataSet.prototype._castItem = function (item, fieldTypes, fields) {
- var clone,
- fieldId = this.fieldId,
- internalIds = this.internalIds;
-
- if (item) {
- clone = {};
- fieldTypes = fieldTypes || {};
-
- if (fields) {
- // output filtered fields
- util.forEach(item, function (value, field) {
- if (fields.indexOf(field) != -1) {
- clone[field] = util.cast(value, fieldTypes[field]);
- }
- });
- }
- else {
- // output all fields, except internal ids
- util.forEach(item, function (value, field) {
- if (field != fieldId || !(value in internalIds)) {
- clone[field] = util.cast(value, fieldTypes[field]);
- }
- });
- }
- }
- else {
- clone = null;
- }
-
- return clone;
- };
-
- /**
- * Update a single item: merge with existing item
- * @param {Object} item
- * @return {String} id
- * @private
- */
- DataSet.prototype._updateItem = function (item) {
- var id = item[this.fieldId];
- if (id == undefined) {
- throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
- }
- var d = this.data[id];
- if (d) {
- // merge with current item
- for (var field in item) {
- if (item.hasOwnProperty(field)) {
- var type = this.fieldTypes[field]; // type may be undefined
- d[field] = util.cast(item[field], type);
- }
- }
- }
- else {
- // create new item
- this._addItem(item);
- }
-
- return id;
- };
-
- /**
- * Get an array with the column names of a Google DataTable
- * @param {DataTable} dataTable
- * @return {Array} columnNames
- * @private
- */
- DataSet.prototype._getColumnNames = function (dataTable) {
- var columns = [];
- for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
- columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
- }
- return columns;
- };
-
- /**
- * Append an item as a row to the dataTable
- * @param dataTable
- * @param columns
- * @param item
- * @private
- */
- DataSet.prototype._appendRow = function (dataTable, columns, item) {
- var row = dataTable.addRow();
- columns.forEach(function (field, col) {
- dataTable.setValue(row, col, item[field]);
- });
- };
-
- // exports
- module.exports = exports = DataSet;
|