UNPKG

jsdataset

Version:

DataSet (like .Net) made available for javascript and more

1,560 lines (1,394 loc) 103 kB
/** * Created by Gaetano Lazzo on 07/02/2015. * Thanks to lodash, ObjectObserve */ /* jslint nomen: true */ /* jslint bitwise: true */ /*globals Environment,jsDataAccess,Function,jsDataQuery,define,_ */ (function (_, dataQuery) { 'use strict'; //noinspection JSUnresolvedVariable /** Detect free variable `global` from Node.js. */ let freeGlobal = typeof global === 'object' && global && global.Object === Object && global; //const freeGlobal = freeExports && freeModule && typeof global === 'object' && global; /** Detect free variable `self`. */ let freeSelf = typeof self === 'object' && self && self.Object === Object && self; /** Used as a reference to the global object. */ let root = freeGlobal || freeSelf || Function('return this')(); /** Detect free variable `exports`. */ let freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; /** Detect free variable `module`. */ let freeModule = freeExports && typeof module === 'object' && module && !module.nodeType && module; //noinspection JSUnresolvedVariable /** Detect free variable `global` from Node.js or Browserified code and use it as `root`. (thanks lodash)*/ let moduleExports = freeModule && freeModule.exports === freeExports; /** * @property CType * @public * @enum CType */ const CType = { 'byteArray':'byteArray', 'string': 'string', 'int': 'int', 'number': 'number', 'date': 'date', 'bool': 'bool', 'unknown': 'unknown' }; //noinspection JSValidateTypes /** * @public * @enum DataRowState */ const DataRowState = { detached: "detached", deleted: "deleted", added: "added", unchanged: "unchanged", modified: "modified" }, /** * Enumerates possible version of a DataRow field: original, current * @public * @enum DataRowVersion */ DataRowVersion = { original: "original", current: "current" }; function dataRowDefineProperty(r, target, property,value) { if (r.removed.hasOwnProperty(property)) { //adding a property that was previously removed if (r.removed[property] !== value) { //if the property had been removed with a different value, that values now goes into // old values r.old[property] = r.removed[property]; } delete r.removed[property]; } else { r.added[property] = value; } } const proxyObjectRow = { get: function(target, prop, receiver) { if (typeof prop === 'symbol'){ return target[prop]; } if (target.getRow && prop.startsWith('$')) { //&& typeof prop === 'string' if (prop === "$acceptChanges") { return () => target.getRow().acceptChanges(); } if (prop === "$rejectChanges") { return () => target.getRow().rejectChanges(); } if (prop === "$del") { return () => target.getRow().del(); } if (prop === "$DataRow") { return target.getRow(); } } return target[prop]; }, set: function(target, property, value, receiver) { if (!target.getRow) { return false; } let r = target.getRow(); if (!r){ return false; } if (!target.hasOwnProperty(property)){ dataRowDefineProperty(r,target,property); } //if property is added, old values has not to be set if (!r.added.hasOwnProperty(property)) { if (!r.old.hasOwnProperty(property)) {//only original value has to be saved r.old[property] = target[property]; } else { if (r.old[property] === value) { delete r.old[property]; } } } target[property]=value; return true; }, defineProperty: function(target, property, descriptor) { if (!target.getRow) { return false; } let r = target.getRow(); if (!r){ return false; } dataRowDefineProperty(r,target,property,target[property]); return Reflect.defineProperty(target, property, descriptor); }, deleteProperty: function(target, property) { if (!target.getRow) { return false; } let r = target.getRow(); if (!r){ return false; } // property; // a property which has been been removed from obj // getOldValueFn(property); // its old value if (r.added.hasOwnProperty(property)) { delete r.added[property]; } else { if (r.old.hasOwnProperty(property)) { //removing a property that had been previously modified r.removed[property] = r.old[property]; } else { r.removed[property] = target[property]; } } delete target[property]; return true; }, }; /** * @public * @class DataColumn * @param {string} columnName * @param {CType} ctype type of the column field **/ function DataColumn(columnName, ctype) { /** * name of the column * @property {string} name **/ this.name = columnName; /** * type of the column * @property {CType} ctype **/ this.ctype = ctype; /** * Skips this column on insert copy * @type {boolean} */ //this.skipInsertCopy = false; /** * column name for posting to db * @property {string} forPosting **/ this.forPosting= undefined; } /** * DataRow shim, provides methods to manage objects as Ado.Net DataRows * @module DataSet * @submodule DataRow */ /** * class type to host data * @public * @class ObjectRow */ function ObjectRow() { return null; } ObjectRow.prototype = { constructor: ObjectRow, /** * Gets the DataRow linked to an ObjectRow * @public * @method getRow * @returns {DataRow} */ getRow : function () { return null; } }; /** * Provides methods to manage objects as Ado.Net DataRows * Creates a DataRow from a generic plain object * @class * @name DataRow * @param {object} o this is the main object managed by the application logic, it is attached to a getRow function */ function DataRow(o) { if (o.constructor === DataRow) { throw 'Called DataRow with a DataRow as input parameter'; } if (o.getRow) { if (this && this.constructor === DataRow) { o = _.clone(o); //throw 'Called new DataRow with an object already attached to a DataRow'; } else { return o.getRow(); } } if (this === undefined || this.constructor !== DataRow) { return new DataRow(o); } if (!o || typeof o !== 'object') { throw ('DataRow(o) needs an object as parameter'); } /** * previous values of the DataRow, only previous values of changed fields are stored * @internal * @property {object} old */ this.old = {}; /** * fields added to object (after last acceptChanges()) * @internal * @property {object} added */ this.added = {}; /** * fields removed (with delete o.field) from object (after last acceptChanges()) * @internal * @property {object} removed */ this.removed = {}; this.myState = DataRowState.unchanged; let that = this; /** * State of the DataRow, possible values are added unchanged modified deleted detached * @public * @property state * @type DataRowState */ Object.defineProperty(this, 'state', { get: function () { if (that.myState === DataRowState.modified || that.myState === DataRowState.unchanged) { if (Object.keys(that.old).length === 0 && Object.keys(that.added).length === 0 && Object.keys(that.removed).length === 0) { that.myState = DataRowState.unchanged; } else { that.myState = DataRowState.modified; } } return that.myState; }, set: function (value) { that.myState = value; }, enumerable: false }); /** * Get the DataRow attached to an object. This method is attached to the object itself, * so you can get the DataRow calling o.getRow() where o is the plain object * This transforms o into an ObjectRow */ Object.defineProperty(o, 'getRow', { value: function () { return that; }, enumerable: false, configurable: true //allows a successive deletion of this property }); /** * @public * @property {ObjectRow} current current value of the DataRow is the ObjectRow attached to it */ this.current = new Proxy(o,proxyObjectRow); /** * @public * @property {DataTable} table */ this.table=undefined; } /** * @type {DataRow} */ DataRow.prototype = { constructor: DataRow, /** * get the value of a field of the object. If dataRowVer is omitted, it's equivalent to o.fieldName * @method getValue * @param {string} fieldName * @param {DataRowVersion} [dataRowVer='current'] possible values are 'original', 'current' * @returns {object} */ getValue: function (fieldName, dataRowVer) { if (dataRowVer === DataRowVersion.original) { if (this.old.hasOwnProperty(fieldName)) { return this.old[fieldName]; } if (this.removed.hasOwnProperty(fieldName)) { return this.removed[fieldName]; } if (this.added.hasOwnProperty(fieldName)) { return undefined; } } return this.current[fieldName]; }, /** * Gets the original row, before changes was made, undefined if current state is added * @method originalRow * @return {object} */ originalRow: function () { if (this.state === DataRowState.unchanged || this.state === DataRowState.deleted) { return this.current; } if (this.state === DataRowState.added) { return undefined; } let o = {}, that = this; _.forEach(_.keys(this.removed), function (k) { o[k] = that.removed[k]; }); _.forEach(_.keys(this.old), function (k) { o[k] = that.old[k]; }); _.forEach(_.keys(this.current), function (k) { if (that.added.hasOwnProperty(k)) { return; //not part of original row } if (that.old.hasOwnProperty(k)) { return; //not part of original row } o[k] = that.current[k]; }); return o; }, /** * Make this row identical to another row (both in state, original and current value) * @param r {DataRow} * @return {DataRow} */ makeSameAs: function (r) { if (this.state === DataRowState.deleted) { this.rejectChanges(); } if (r.state === DataRowState.deleted) { return this.makeEqualTo(r.originalRow()).acceptChanges().del(); } if (r.state === DataRowState.unchanged) { return this.makeEqualTo(r.current).acceptChanges(); } if (r.state === DataRowState.modified) { return this.makeEqualTo(r.originalRow()).acceptChanges().makeEqualTo(r.current); } if (r.state === DataRowState.added) { //assumes this also is already in the state of "added" let res= this.makeEqualTo(r.current); res.state=DataRowState.added; return res; } return this; }, /** * changes current row to make it's current values equal to another one. Deleted rows becomes modified * compared to patchTo, this also removes values that are not present in other row * @method makeEqualTo * @param {object} o * @return {DataRow} */ makeEqualTo: function (o) { /** * @type {DataRow} */ let that = this; if (this.state === DataRowState.deleted) { this.rejectChanges(); } //removes properties in this that are not present in o _.forEach(_.keys(this.current), function (k) { if (!o.hasOwnProperty(k)) { delete that.current[k]; } }); //get all properties from o _.forEach(_.keys(o), function (k) { that.current[k] = o[k]; }); return that; }, /** * changes current row to make its current values equal to another one. Deleted rows becomes modified * @method patchTo * @param {object} o * @return {DataRow} */ patchTo: function (o) { let that = this; if (this.state === DataRowState.deleted) { this.rejectChanges(); } //get all properties from o _.forEach(_.keys(o), function (k) { that.current[k] = o[k]; }); return this; }, /** * Get the column name of all modified/added/removed fields * @return {*} */ getModifiedFields: function () { return _.union(_.keys(this.old), _.keys(this.removed), _.keys(this.added)); }, /** * Makes changes permanents, discarding old values. state becomes unchanged, detached remains detached * @method acceptChanges * @return {DataRow} */ acceptChanges: function () { if (this.state === DataRowState.detached) { return this; } if (this.state === DataRowState.deleted) { this.detach(); return this; } this.reset(); return this; }, /** * Discard changes, restoring the original values of the object. state becomes unchanged, * detached remains detached * @method rejectChanges * @return {DataRow} */ rejectChanges: function () { if (this.state === DataRowState.detached) { return this; } if (this.state === DataRowState.added) { this.detach(); return this; } _.extend(this.current, this.old); let that = this; _.forEach(this.added, function (value, fieldToDel) { delete that.current[fieldToDel]; }); _.forEach(this.removed, function (value, fieldToAdd) { that.current[fieldToAdd] = that.removed[fieldToAdd]; }); this.reset(); return this; }, /** * resets all change and sets state to unchanged * @private * @method _reset * @return {DataRow} */ reset: function () { this.old = {}; this.added = {}; this.removed = {}; this.state = DataRowState.unchanged; return this; }, /** * Detaches row, loosing all changes made. object is also removed from the underlying DataTable. * Proxy is disposed. * @method detach * @return {undefined} */ detach: function () { this.state = DataRowState.detached; if (this.table) { //this calls row.detach this.table.detach(this.current); return undefined; } delete this.current.getRow; return undefined; }, /** * Deletes the row. If it is in added state it becomes detached. Otherwise any changes are lost, and * only rejectChanges can bring the row into life again * @method del * @returns {DataRow} */ del: function () { if (this.state === DataRowState.deleted) { return this; } if (this.state === DataRowState.added) { this.detach(); return this; } if (this.state === DataRowState.detached) { return this; } this.rejectChanges(); this.state = DataRowState.deleted; return this; }, /** * Debug - helper function * @method toString * @returns {string} */ toString: function () { if (this.table) { return 'DataRow of table ' + this.table.name + ' (' + this.state + ')'; } return 'DataRow' + ' (' + this.state + ')'; }, /** * Gets the parent(s) of this row in the dataSet it is contained, following the relation with the * specified name * @method getParentRows * @param {string} relName * @returns {ObjectRow[]} */ getParentRows: function (relName) { let rel = this.table.dataset.relations[relName]; if (rel === undefined) { throw 'Relation ' + relName + ' does not exists in dataset ' + this.table.dataset.name; } return rel.getParents(this.current); }, /** * Gets all parent rows of this one * @returns {ObjectRow[]} */ getAllParentRows: function () { let that = this; return _(this.table.dataset.relationsByChild[this.table.name]) .value() .reduce(function (list, rel) { return list.concat(rel.getParents(that.current)); }, [], this); }, /** * Gets parents row of this row in a given table * @method getParentsInTable * @param {string} parentTableName * @returns {ObjectRow[]} */ getParentsInTable: function (parentTableName) { let that = this; return _(this.table.dataset.relationsByChild[this.table.name]) .filter({parentTable: parentTableName}) .value() .reduce(function (list, rel) { return list.concat(rel.getParents(that.current)); }, [], this); }, /** * Gets the child(s) of this row in the dataSet it is contained, following the relation with the * specified name * @method getChildRows * @param {string} relName * @returns {ObjectRow[]} */ getChildRows: function (relName) { let rel = this.table.dataset.relations[relName]; if (rel === undefined) { throw 'Relation ' + relName + ' does not exists in dataset ' + this.table.dataset.name; } return rel.getChild(this.current); }, /** * Gets all child rows of this one * @returns {ObjectRow[]} */ getAllChildRows: function () { let that = this; return _(this.table.dataset.relationsByParent[this.table.name]) .value() .reduce(function (list, rel) { return list.concat(rel.getChild(that.current)); }, []); }, /** * Gets child rows of this row in a given table * @method getChildInTable * @param {string} childTableName * @returns {ObjectRow[]} */ getChildInTable: function (childTableName) { let that = this; return _(this.table.dataset.relationsByParent[this.table.name]) .filter({childTable: childTableName}) .value() .reduce(function (list, rel) { return list.concat(rel.getChild(that.current)); }, []); }, /** * DataTable that contains this DataRow * @property table * @type DataTable */ /** * Get an object with all key fields of this row * @method keySample * @returns {object} */ keySample: function () { return _.pick(this.current, this.table.key); } }; /** * Describe how to evaluate the value of a column before posting it * @constructor AutoIncrementColumn * @param {string} columnName * @param {object} options same options as AutoIncrementColumn properties **/ function AutoIncrementColumn(columnName, options) { /** * name of the column that has to be auto-incremented * @property {string} columnName */ this.columnName = columnName; /** * Array of column names of selector fields. The max() is evaluating filtering the values of those fields * @property {string[]} [selector] */ this.selector = options.selector || []; /** * Array of bit mask to use for comparing selector. If present, only corresponding bits will be compared, * i.e. instead of sel=value it will be compared (sel & mask) = value * @property {number[]} [selectorMask] **/ this.selectorMask = options.selectorMask || []; /** * A field to use as prefix for the evaluated field * @property {string} [prefixField] **/ this.prefixField = options.prefixField; /** * String literal to be appended to the prefix before the evaluated max * @property {string} [middleConst] **/ this.middleConst = options.middleConst; /** * for string id, the len of the evaluated max. It is not the overall size of the evaluated id, because a * prefix and a middle const might be present * If idLen = 0 and there is no prefix, the field is assumed to be a number, otherwise a 0 prefixed string-number * @property {number} [idLen=0] **/ this.idLen = options.idLen || 0; /** * Indicates that numbering does NOT depend on prefix value, I.e. is linear in every section of the calculated field * @property {boolean} [linearField=false] **/ this.linearField = options.linearField || false; /** * Minimum temporary value for in-memory rows * @property {number} [minimum=0] **/ this.minimum = options.minimum || 0; /** * true if this field is a number * @property {number} [isNumber=false] **/ if (options.isNumber === undefined) { this.isNumber = (this.idLen === 0) && (this.prefixField === undefined) && (this.middleConst === undefined); } else { this.isNumber = options.isNumber; } if (this.isNumber === false && this.idLen === 0) { this.idLen = 12; //get a default for idLen } } AutoIncrementColumn.prototype = { constructor: AutoIncrementColumn }; /** * Gets a function that filter selector fields eventually masking with selectorMask * @param row * @returns {sqlFun} */ AutoIncrementColumn.prototype.getFieldSelectorMask = function (row) { let that = this; if (this.getInternalSelector === undefined) { this.getInternalSelector = function (r) { return dataQuery.and( _.map(that.selector, function (field, index) { if (that.selectorMask && that.selectorMask[index]) { return dataQuery.testMask(field, that.selectorMask[index], r[field]); } else { return dataQuery.eq(field, r[field]); } }) ); }; } return this.getInternalSelector(row); }; /** * evaluates the function to filter selector on a specified row and column * @method getSelector * @param {ObjectRow} r * @returns {sqlFun} */ AutoIncrementColumn.prototype.getSelector = function (r) { let prefix = this.getPrefix(r), selector = this.getFieldSelectorMask(r); if (this.linearField === false && prefix !== '') { selector = dataQuery.and(selector, dataQuery.like(this.columnName, prefix + '%')); } return selector; }; /** * Gets the prefix evaluated for a given row * @method getPrefix * @param r * @returns string */ AutoIncrementColumn.prototype.getPrefix = function (r) { let prefix = ''; if (this.prefixField) { if (r[this.prefixField] !== null && r[this.prefixField] !== undefined) { prefix += r[this.prefixField]; } } if (this.middleConst) { prefix += this.middleConst; } return prefix; }; /** * gets the expression to be used for retrieving the max * @method getExpression * @param {ObjectRow} r * @return {sqlFun} */ AutoIncrementColumn.prototype.getExpression = function (r) { let fieldExpr = dataQuery.field(this.columnName), lenToExtract, startSearch; if (this.isNumber) { return dataQuery.max(fieldExpr); } startSearch = this.getPrefix(r).length; lenToExtract = this.idLen; return dataQuery.max(dataQuery.convertToInt(dataQuery.substring(fieldExpr, startSearch + 1, lenToExtract))); }; /** * Optional custom function to be called to evaluate the maximum value * @method customFunction * @param {ObjectRow} r * @param {string} columnName * @param {jsDataAccess} conn * @return {object} **/ AutoIncrementColumn.prototype.customFunction = null; /** * A DataTable is s collection of ObjectRow and provides information about the structure of logical table * @class * @name DataTable * @param {string} tableName * @constructor * @return {DataTable} */ function DataTable(tableName) { /** * Name of the table * @property {string} name */ this.name = tableName; /** * Collection of rows, each one hiddenly surrounded with a DataRow object * @property rows * @type ObjectRow[] */ this.rows = []; /** * Array of key column names * @private * @property {string[]} myKey */ this.myKey = []; /** * Set of properties to be assigned to new rows when they are created * @property {object} myDefaults * @private */ this.myDefaults = {}; /** * Dictionary of DataColumn * @property columns * @type {{DataColumn}} */ this.columns = {}; /** * @property autoIncrementColumns * @type {{AutoIncrementColumn}} */ this.autoIncrementColumns = {}; /** * DataSet to which this table belongs * @property {DataSet} dataset */ this.dataset = undefined; /** * A ordering to use for posting of this table * @property postingOrder * @type string | string[] | function */ } DataTable.prototype = { constructor: DataTable, /** * @private * @property maxCache * @type object */ /** * Mark the table as optimized / not optimized * An optimized table has a cache for all autoincrement field * @method setOptimize * @param {boolean} value */ setOptimized: function (value) { if (value === false) { delete this.maxCache; return; } if (this.maxCache === undefined) { this.maxCache = {}; } }, /** * Check if this table is optimized * @method isOptimized * @returns {boolean} */ isOptimized: function () { return this.maxCache !== undefined; }, /** * Clear evaluated max cache * @method clearMaxCache */ clearMaxCache: function () { if (this.maxCache !== undefined) { this.maxCache = {}; } }, /** * Get name to be used where columns are written to the database. Usually the same as column name, * but can differ if the real table is a different one * @param {string[]}colNames */ getPostingColumnsNames: function (colNames){ if (this.postingTable()===this.name){ return colNames; } return _.map(colNames, c=>{ let col=this.columns[c]; if (col===undefined) { return c; } return col.forPosting || c; }); }, /** * Set a value in the max cache * @method setMaxExpr * @param {string} field * @param {sqlFun} expr * @param {sqlFun} filter * @param {int} num */ setMaxExpr: function (field, expr, filter, num) { if (this.maxCache === undefined) { return; } let hash = field + '§' + expr.toString() + '§' + filter.toString(); this.maxCache[hash] = num; }, /** * * @param {string} name * @param {CType} ctype * @return {DataColumn} */ setDataColumn: function (name, ctype) { let c = this.columns[name]; if (c){ c.ctype= ctype; } else { c= new DataColumn(name, ctype); } this.columns[name] = c; return c; }, /** * get/set the minimum temp value for a field, assuming 0 if undefined * @method minimumTempValue * @param {string} field * @param {number} [value] */ minimumTempValue: function (field, value) { let autoInfo = this.autoIncrementColumns[field]; if (autoInfo === undefined) { if (value === undefined) { return 0; } this.autoIncrementColumns[field] = new AutoIncrementColumn(field, {minimum: value}); } else { if (value === undefined) { return autoInfo.minimum || 0; } autoInfo.minimum = value; } }, /** * gets the max in cache for a field and updates the cache * @method getMaxExpr *@param {string} field * @param {sqlFun|string}expr * @param {sqlFun} filter * @return {number} */ getMaxExpr: function (field, expr, filter) { let hash = field + '§' + expr.toString() + '§' + filter.toString(), res = this.minimumTempValue(field); if (this.maxCache[hash] !== undefined) { res = this.maxCache[hash]; } this.maxCache[hash] = res + 1; return res; }, /** * Evaluates the max of an expression eventually using a cached value * @method cachedMaxSubstring * @param {string} field * @param {number} start * @param {number} len * @param {sqlFun} filter * @return {number} */ cachedMaxSubstring: function (field, start, len, filter) { let expr; if (!this.isOptimized()) { return this.unCachedMaxSubstring(field, start, len, filter); } expr = field + '§' + start + '§' + len + '§' + filter.toString(); return this.getMaxExpr(field, expr, filter); }, /** * Evaluates the max of an expression without using any cached value. If len = 0 the expression is managed * as a number with max(field) otherwise it is performed max(convertToInt(substring(field,start,len))) * @param {string} field * @param {number} start * @param {number} len * @param {sqlFun} filter * @return {number} */ unCachedMaxSubstring: function (field, start, len, filter) { let res, min = this.minimumTempValue(field), expr, rows; if (start === 0 && len === 0) { expr = dataQuery.max(field); } else { expr = dataQuery.max(dataQuery.convertToInt(dataQuery.substring(field, start, len))); } rows = this.selectAll(filter); if (rows.length === 0) { res = 0; } else { res = expr(rows); } if (res < min) { return min; } return res; }, /** * Extract a set of rows matching a filter function - skipping deleted rows * @method select * @param {sqlFun} [filter] * @returns {ObjectRow[]} */ select: function (filter) { if (filter === null || filter === undefined) { return _.filter(this.rows, function (r) { return r.getRow().state !== DataRowState.deleted; }); } if (filter) { if (filter.isTrue) { //console.log("always true: returning this.rows"); //does not return deleted rows, coherently with other cases return _.filter(this.rows, function (r) { return r.getRow().state !== DataRowState.deleted; }); //return this.rows; } if (filter.isFalse) { //console.log("always false: returning []"); return []; } } return _.filter(this.rows, function (r) { //console.log('actually filtering by '+filter); if (r.getRow().state === DataRowState.deleted) { //console.log("skipping a deleted row"); return false; } if (filter) { //console.log('filter(r) is '+filter(r)); //noinspection JSValidateTypes because a sqlFun is also a Function return filter(r); } return true; }); }, /** * Extract a set of rows matching a filter function - including deleted rows * @method selectAll * @param {sqlFun} filter * @returns {ObjectRow[]} */ selectAll: function (filter) { if (filter) { return _.filter(this.rows, filter); } return this.rows; }, /** * Get the filter that compares key fields of a given row * @method keyFilter * @param {object} row * @returns {*|sqlFun} */ keyFilter: function (row) { if (this.myKey.length === 0) { throw 'No primary key specified for table:' + this.name + ' and keyFilter was invoked.'; } return dataQuery.mcmp(this.myKey, row); }, /** * Compares the key of two objects * @param {object} a * @param {object} b * @returns {boolean} */ sameKey: function (a, b) { return _.find(this.myKey, function (k) { return a[k] !== b[k]; }) !== undefined; }, /** * Get/Set the primary key in a Jquery fashioned style. If k is given, the key is set, otherwise the existing * key is returned * @method key * @param {string[]} [k] * @returns {*|string[]} */ key: function (k) { if (k === undefined) { return this.myKey; } if (_.isArray(k)) { this.myKey = _.clone(k); } else { this.myKey = Array.prototype.slice.call(arguments); } _.forEach(this.columns,function(c){ delete c.isPrimaryKey; }); let self=this; _.forEach(this.myKey,function(k){ if (!self.columns[k]){ return true; } self.columns[k].isPrimaryKey=true; }); return this; }, /** * Check if a column is key * @param {string} k * @returns {boolean} */ isKey: function(k){ if (this.columns[k]){ return this.columns[k].isPrimaryKey; } return this.myKey.indexOf(k)>=0; }, /** * Clears the table detaching all rows. * @method clear */ clear: function () { let dr; _.forEach(this.rows, function (row) { dr = row.getRow(); dr.table = null; dr.detach(); }); this.rows.length = 0; }, /** * Detaches a row from the table * @method detach * @param obj */ detach: function (obj) { if (!obj.acceptChanges){//non è il proxy, ottiene il proxy dal DataRow associato obj = obj.getRow().current; } let i = this.rows.indexOf(obj), dr; if (i >= 0) { this.rows.splice(i, 1); } dr = obj.getRow(); dr.table = null; dr.detach(); }, /** * Adds an object to the table setting the datarow in the state of "added" * @method add * @param obj plain object * @returns DataRow created */ add: function (obj) { let dr = this.load(obj); if (dr.state === DataRowState.unchanged) { dr.state = DataRowState.added; } return dr; }, /** * check if a row is present in the table. If there is a key, it is used for finding the row, * otherwise a ==== comparison is made * @method existingRow * @param {Object} obj * @return {DataRow | undefined} */ existingRow: function (obj) { if (obj.getRow && !obj.acceptChanges){ obj = obj.getRow().current; } if (this.myKey.length === 0) { let i = this.rows.indexOf(obj); if (i === -1) { return undefined; } return this.rows[i]; } let arr = _.filter(this.rows, this.keyFilter(obj)); if (arr.length === 0) { return undefined; } return arr[0]; }, /** * Adds an object to the table setting the datarow in the state of "unchanged" * @method load * @param {object} obj plain object to load in the table * @param {boolean} [safe=true] if false doesn't verify existence of row * @returns {DataRow} created DataRow */ load: function (obj, safe) { let dr, oldRow; if (safe || safe === undefined) { oldRow = this.existingRow(obj); if (oldRow) { return oldRow.getRow(); } } dr = new DataRow(obj); dr.table = this; this.rows.push(dr.current); return dr; }, /** * Adds an object to the table setting the datarow in the state of 'unchanged' * @method loadArray * @param {object[]} arr array of plain objects * @param {boolean} safe if false doesn't verify existence of row * @return * */ loadArray: function (arr, safe) { let that = this; _.forEach(arr, function (o) { that.load(o, safe); }); }, /** * Accept any changes setting all dataRows in the state of 'unchanged'. * Deleted rows become detached and are removed from the table * @method acceptChanges */ acceptChanges: function () { //First detach all deleted rows let newRows = []; _.forEach(this.rows, /** * @type {ObjectRow} * @param o */ function (o) { let dr = o.getRow(); if (dr.state === DataRowState.deleted) { dr.table = null; dr.detach(); } else { dr.acceptChanges(); newRows.push(o); } }); this.rows = newRows; }, /** * Reject any changes putting all to 'unchanged' state. * Added rows become detached. * @method rejectChanges */ rejectChanges: function () { //First detach all added rows let newRows = []; _(this.rows).forEach( /** * @method * @param {ObjectRow} o */ function (o) { let dr = o.getRow(); if (dr.state === DataRowState.added) { dr.table = null; dr.detach(); } else { dr.rejectChanges(); newRows.push(o); } }); this.rows = newRows; }, /** * Check if any DataRow in the table has changes * @method hasChanges * @returns {boolean} */ hasChanges: function () { return _.some(this.rows, function (o) { return o.getRow().state !== DataRowState.unchanged; }); }, /** * gets an array of all modified/added/deleted rows * @method getChanges * @returns {Array} */ getChanges: function () { return _.filter(this.rows, function (o) { return o.getRow().state !== DataRowState.unchanged; }); }, /** * Debug-helper function * @method toString * @returns {string} */ toString: function () { return "DataTable " + this.name; }, /** * import a row preserving it's state, the row should already have a DataRow attached * @method importRow * @param {object} row input * @returns {DataRow} created */ importRow: function (row) { let dr = row.getRow(), newR, newDr; newR = {}; _.forOwn(row, function (val, key) { newR[key] = val; }); newDr = new DataRow(newR); //this creates an observer on newR newDr.state = dr.state; newDr.old = _.clone(dr.old, true); newDr.added = _.clone(dr.added, true); newDr.removed = _.clone(dr.removed, true); this.rows.push(newR); return newDr; }, /** * Get/set the object defaults in a JQuery fashioned style. When def is present, its fields and values are * merged into existent defaults. * @method defaults * @param [def] * @returns {object|*} */ defaults: function (def) { if (def === undefined) { return this.myDefaults; } _.assign(this.myDefaults, def); return this; }, /** * Clears any stored default value for the table * @method clearDefaults */ clearDefaults: function () { this.myDefaults = {}; }, /** * creates a DataRow and returns the created object. The created object has the default values merged to the * values in the optional parameter obj. * @method newRow * @param {object} [obj] contains the initial value of the created objects. * @param {ObjectRow} [parentRow] * @returns {object} */ newRow: function (obj, parentRow) { let n = {}; _.assign(n, this.myDefaults); if (_.isObject(obj)) { _.assign(n, obj); } if (parentRow !== undefined) { this.makeChild(n, parentRow.getRow().table.name, parentRow); } this.calcTemporaryId(n); return this.add(n).current; }, /** * Make childRow child of parentRow if a relation between the two is found * @method makeChild * @param {object} childRow * @param {string} parentTable * @param {ObjectRow} [parentRow] */ makeChild: function (childRow, parentTable, parentRow) { let that = this, parentRel = _.find(this.dataset.relationsByParent[parentTable], function (rel) { return rel.childTable === that.name; }); if (parentRel === undefined) { return; } parentRel.makeChild(parentRow, childRow); }, /** * Get/Set a flag indicating that this table is not subjected to security functions in a jQuery fashioned * style * @method skipSecurity * @param {boolean} [arg] * @returns {*