jsdataset
Version:
DataSet (like .Net) made available for javascript and more
1,560 lines (1,394 loc) • 103 kB
JavaScript
/**
* 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 {*