hedera-web/web/js/db/model.js

1228 lines
24 KiB
JavaScript
Executable File

/**
* Class to handle the Database select results. Also allows
* updates, insertions and deletions on tables where the primary key is
* selected.
*
* Note that table and column names must be unique in the selection query,
* otherwise updates are not allowed on that table/column. If two tables or
* columns have the same name, an alias should be used to make it updatable.
**/
Db.Model = new Class ().extend
({
Status:
{
CLEAN : 1
,LOADING : 2
,READY : 3
,ERROR : 4
},
Mode:
{
ON_CHANGE: 1
,ON_DEMAND: 2
},
Operation:
{
INSERT: 1 << 1
,UPDATE: 1 << 2
,DELETE: 1 << 3
}
,SortWay:
{
ASC : 1,
DESC : 2
}
});
Db.Model.implement
({
Extends: Vn.Object
,Tag: 'db-model'
,Properties:
{
/**
* The connection used to execute the statement.
**/
conn:
{
type: Db.Conn
,set: function (x)
{
this._conn = x;
this._autoLoad ();
}
,get: function ()
{
return this._conn;
}
},
/**
* The result index.
**/
resultIndex:
{
type: Number
,set: function (x)
{
this._resultIndex = x;
}
,get: function ()
{
return this._resultIndex;
}
},
/**
* The batch used to execute the statement.
**/
batch:
{
type: Sql.Batch
,set: function (x)
{
this.link ({_batch: x}, {'changed': this._autoLoad});
this._autoLoad ();
}
,get: function ()
{
return this._batch;
}
},
/**
* The model select statement.
**/
stmt:
{
type: Sql.Stmt
,set: function (x)
{
this._stmt = x;
this._autoLoad ();
}
,get: function ()
{
return this._stmt;
}
},
/**
* The model query.
**/
query:
{
type: String
,set: function (x)
{
this._stmt = new Sql.String ({query: x});
}
,get: function ()
{
return this._stmt.render (null);
}
},
/**
* The main table.
**/
mainTable:
{
type: String
,set: function (x)
{
this._mainTable = null;
this.requestedMainTable = x;
this._refreshMainTable ();
}
,get: function ()
{
return this._mainTable;
}
},
/**
* Determines if the model is updatable.
**/
updatable:
{
type: Boolean
,set: function (x)
{
this._updatable = false;
this.requestedUpdatable = x;
this._refreshUpdatable ();
}
,get: function ()
{
return this._updatable;
}
},
/**
* The number of rows in the model.
**/
numRows:
{
type: Number
,get: function ()
{
if (this.data)
return this.data.length;
return 0;
}
},
/**
* The current status of the model.
**/
status:
{
type: Number
,get: function ()
{
return this._status;
}
},
/**
* Checks if the model data is ready.
**/
ready:
{
type: Boolean
,get: function ()
{
return this._status == Db.Model.Status.READY;
}
},
/**
* Update mode.
**/
mode:
{
enumType: Db.Model.Mode
,value: Db.Model.Mode.ON_CHANGE
},
/**
* Wether to execute the model query automatically.
**/
autoLoad:
{
type: Boolean
,value: true
}
}
,_resultIndex: 0
,_batch: null
,_stmt: null
,_status: Db.Model.Status.CLEAN
,data: null
,tables: null
,columns: null
,columnMap: null
,_updatable: false
,sortColumn: -1
,sortWay: null
,requestedIndexes: {}
,indexes: []
,requestedUpdatable: false
,operations: null
,operationsMap: null
,defaults: []
,requestedMainTable: null
,initialize: function (props)
{
this.parent (props);
this._cleanData ();
this._setStatus (Db.Model.Status.CLEAN);
}
,loadXml: function (builder, node)
{
this.parent (builder, node);
var query = node.firstChild.nodeValue;
if (query)
this.query = query;
}
,_autoLoad: function ()
{
if (this.autoLoad)
this.refresh ();
}
/**
* Refresh the model data reexecuting the query on the database.
**/
,refresh: function ()
{
if (this._stmt && this._batch)
this._stmt.findHolders (this._batch);
if (this._conn && this._stmt
&& (!this._batch || this._batch.isReady ()))
{
this._setStatus (Db.Model.Status.LOADING);
this._conn.execStmt (this._stmt, this._selectDone.bind (this), this._batch);
}
else
{
this._cleanData ();
this._setStatus (Db.Model.Status.CLEAN);
}
}
,_selectDone: function (resultSet)
{
var result;
var dataResult;
this._cleanData ();
for (var i = 0; result = resultSet.fetchResult (); i++)
if (i == this._resultIndex)
dataResult = result;
if (dataResult && typeof dataResult === 'object')
{
this.sortColumn = -1;
this.data = dataResult.data;
this.tables = dataResult.tables;
this.columns = dataResult.columns;
this.columnMap = dataResult.columnMap;
this._repairColumns ();
this._refreshRowIndexes (0);
this._refreshMainTable ();
for (column in this.requestedIndexes)
this._buildIndex (column);
this._setStatus (Db.Model.Status.READY);
}
else
this._setStatus (Db.Model.Status.ERROR);
}
,_refreshRowIndexes: function (start)
{
for (var i = start; i < this.data.length; i++)
this.data[i].index = i;
if (this.operationsMap)
{
this.operationsMap = {};
for (var i = 0; i < this.operations.length; i++)
this.operationsMap[i] = this.operations[i];
}
}
,_cleanData: function (error)
{
this.data = null;
this.tables = null;
this.columns = null;
this.columnMap = null;
this.indexes = [];
this._resetOperations ();
}
,_refreshUpdatable: function ()
{
var oldValue = this._updatable;
this._updatable = this._mainTable !== null && this.requestedUpdatable;
if (oldValue != this._updatable)
this.signalEmit ('updatable-changed');
}
,_refreshMainTable: function ()
{
var newMainTable = null;
var tables = this.tables;
if (tables)
for (var i = 0; i < tables.length; i++)
if (tables[i].pks.length > 0)
if (!this.requestedMainTable
|| tables[i].name === this.requestedMainTable)
{
newMainTable = i;
break;
}
this._mainTable = newMainTable;
this._refreshUpdatable ();
}
/**
* Sets the default value for inserted fields.
*
* @param {String} field The destination field name
* @param {String} table The destination table name
* @param {Sql.Expr} srcColumn The default value expression
**/
,setDefault: function (field, table, expr)
{
this.defaults.push
({
field: field
,table: table
,expr: expr
});
}
/**
* Sets the default value for inserted fields.
*
* @param {String} field The destination field name
* @param {String} table The destination table name
* @param {Object} value The default value
**/
,setDefaultFromValue: function (field, table, value)
{
this.defaults.push
({
field: field
,table: table
,value: value
});
}
/**
* Sets the default value for inserted fields from another column in the
* model.
*
* @param {String} field The destination field name
* @param {String} table The destination table name
* @param {String} srcColumn The source column
**/
,setDefaultFromColumn: function (field, table, srcColumn)
{
this.defaults.push
({
field: field
,table: table
,srcColumn: srcColumn
});
}
/**
* Checks if the column exists.
*
* @param {integer} column The column index
* @return {Boolean} %true if column exists, %false otherwise
**/
,checkColExists: function (column)
{
return this.columns
&& column >= 0
&& column < this.columns.length;
}
/**
* Checks if the row exists.
*
* @param {integer} rowIndex The row index
* @return {Boolean} %true if row exists, %false otherwise
**/
,checkRowExists: function (rowIndex)
{
return this.data
&& rowIndex >= 0
&& rowIndex < this.data.length;
}
,_checkTableUpdatable: function (tableIndex)
{
var tableUpdatable = tableIndex !== null
&& this.tables[tableIndex].pks.length > 0;
if (!tableUpdatable)
console.warn ("Db.Model: Table %s is not updatable",
this.tables[tableIndex].name);
return tableUpdatable;
}
/**
* Get the index of the column from its name.
*
* @param {string} columnName The column name
* @return {number} The column index or -1 if column not exists
**/
,getColumnIndex: function (columnName)
{
var index;
if (this.columnMap
&& (index = this.columnMap[columnName]) !== undefined)
return index;
return -1;
}
/**
* Gets a value from the model.
*
* @param {number} rowIndex The row index
* @param {string} columnName The column name
* @return {mixed} The value
**/
,get: function (rowIndex, columnName)
{
var index = this.getColumnIndex (columnName);
if (index != -1)
return this.getByIndex (rowIndex, index);
return undefined;
}
/**
* Updates a value on the model.
*
* @param {number} rowIndex The row index
* @param {string} columnName The column name
* @param {mixed} value The new value
**/
,set: function (rowIndex, columnName, value)
{
var index = this.getColumnIndex (columnName);
if (index != -1)
this.setByIndex (rowIndex, index, value);
else
console.warn ('Db.Model: Column %s doesn\'t exist', columnName);
}
/**
* Gets a value from the model using the column index.
*
* @param {number} rowIndex The row index
* @param {number} column The column index
* @return {mixed} The value
**/
,getByIndex: function (rowIndex, column)
{
if (this.checkRowExists (rowIndex) && this.checkColExists (column))
return this.data[rowIndex][column];
return undefined;
}
/**
* Updates a value on the model using the column index.
*
* @param {number} rowIndex The row index
* @param {number} col The column index
* @param {mixed} value The new value
**/
,setByIndex: function (rowIndex, col, value)
{
if (!this.checkRowExists (rowIndex)
&& !this.checkColExists (col))
return;
var tableIndex = this.columns[col].table;
if (!this._checkTableUpdatable (tableIndex))
return;
var row = this.data[rowIndex];
var op = this._createOperation (rowIndex);
op.type |= Db.Model.Operation.UPDATE;
if (!op.oldValues)
op.oldValues = [];
if (!op.tables)
op.tables = {};
var tableOp = op.tables[tableIndex];
if (!tableOp)
{
tableOp = Db.Model.Operation.UPDATE;
var pks = this.tables[tableIndex].pks;
for (var i = 0; i < pks.length; i++)
if (!row[pks[i]] && !op.oldValues[pks[i]])
{
tableOp = Db.Model.Operation.INSERT;
break;
}
op.tables[tableIndex] = tableOp;
}
if (tableOp & Db.Model.Operation.UPDATE
&& op.oldValues[col] === undefined)
op.oldValues[col] = row[col];
this.signalEmit ('row-updated-before', rowIndex);
row[col] = value;
this.signalEmit ('row-updated', rowIndex, [col]);
if (this.mode == Db.Model.Mode.ON_CHANGE
&& !(op.type & Db.Model.Operation.INSERT))
this.performOperations ();
}
/**
* Deletes a row from the model.
*
* @param {number} rowIndex The row index
**/
,deleteRow: function (rowIndex)
{
if (!this.checkRowExists (rowIndex)
|| !this._checkTableUpdatable (this._mainTable))
return;
var op = this._createOperation (rowIndex);
op.type |= Db.Model.Operation.DELETE;
if (!this.requestedMainTable)
{
this.signalEmit ('row-deleted-before', rowIndex);
this.data.splice (rowIndex, 1);
this.signalEmit ('row-deleted', rowIndex);
this._refreshRowIndexes (rowIndex);
}
else
{
this.signalEmit ('row-updated-before', rowIndex);
if (!op.oldValues)
op.oldValues = [];
var updatedCols = [];
for (var i = 0; i < this.columns.length; i++)
if (this.columns[i].table == this._mainTable)
{
if (op.oldValues[i] === undefined)
op.oldValues[i] = op.row[i];
op.row[i] = null;
updatedCols.push (i);
}
this.signalEmit ('row-updated', rowIndex, updatedCols);
}
if (this.mode === Db.Model.Mode.ON_CHANGE)
this.performOperations ();
}
/**
* Inserts a new row on the model.
*
* @return The index of the inserted row
**/
,insertRow: function ()
{
if (!this._checkTableUpdatable (this._mainTable))
return -1;
var cols = this.columns;
var newRow = new Array (cols.length);
for (var i = 0; i < cols.length; i++)
if (cols[i].table === this._mainTable)
newRow[i] = cols[i].def;
else
newRow[i] = null;
var rowIndex = this.data.push (newRow) - 1;
newRow.index = rowIndex;
var op = this._createOperation (rowIndex);
op.type |= Db.Model.Operation.INSERT;
this.signalEmit ('row-inserted', rowIndex);
return rowIndex;
}
/**
* Performs all model changes on the database.
**/
,performOperations: function ()
{
var ops = this.operations;
if (ops.length === 0)
{
this.signalEmit ('operations-done');
return;
}
var stmts = new Sql.MultiStmt ();
var query = new Sql.String ({query: 'START TRANSACTION'});
stmts.addStmt (query);
for (var i = 0; i < ops.length; i++)
{
query = null;
var op = ops[i];
if (op.type & Db.Model.Operation.DELETE)
{
if (op.type & Db.Model.Operation.INSERT)
continue;
var where = this._createWhere (this._mainTable, op, true);
if (where)
{
query = new Sql.Delete ({where: where});
query.addTarget (this._createTarget (this._mainTable));
}
}
else if (op.type & (Db.Model.Operation.INSERT | Db.Model.Operation.UPDATE))
{
query = new Sql.MultiStmt ();
for (var tableIndex in op.tables)
{
var stmt = this._createDmlQuery (op, parseInt (tableIndex));
query.addStmt (stmt);
}
}
if (query)
{
stmts.addStmt (query);
}
else
{
console.warn ('Db.Model: %s', _('ErrorSavingChanges'));
return;
}
}
var query = new Sql.String ({query: 'COMMIT'});
stmts.addStmt (query);
this._conn.execStmt (stmts,
this._onOperationsDone.bind (this, ops));
this._resetOperations ();
}
,_createDmlQuery: function (op, tableIndex)
{
var where = this._createWhere (tableIndex, op, false);
if (!where)
return null;
var multiStmt = new Sql.MultiStmt ();
var target = this._createTarget (tableIndex);
var select = new Sql.Select ({where: where});
select.addTarget (target);
var row = op.row;
var cols = this.columns;
if (op.tables[tableIndex] & Db.Model.Operation.INSERT)
{
var dmlQuery = new Sql.Insert ();
var table = this.tables[tableIndex];
for (var i = 0; i < this.defaults.length; i++)
{
var def = this.defaults[i];
if (def.table === table.name)
{
if (def.value)
dmlQuery.addSet (def.field, def.value);
else if (def.expr)
dmlQuery.addExpr (def.field, def.expr);
else if (def.srcColumn)
{
var columnIndex = this.getColumnIndex (def.srcColumn);
dmlQuery.addSet (def.field, row[columnIndex]);
}
}
}
for (var i = 0; i < cols.length; i++)
if (cols[i].table === tableIndex)
{
if (row[i] !== null)
dmlQuery.addSet (cols[i].orgname, row[i]);
select.addField (cols[i].orgname);
}
}
else
{
var updateWhere = this._createWhere (tableIndex, op, true);
if (!updateWhere)
return null;
var dmlQuery = new Sql.Update ({where: updateWhere});
for (var i = 0; i < cols.length; i++)
if (cols[i].table === tableIndex && op.oldValues[i] !== undefined)
{
var fieldName = cols[i].orgname;
dmlQuery.addSet (fieldName, row[i]);
select.addField (fieldName);
}
}
dmlQuery.addTarget (target);
multiStmt.addStmt (dmlQuery);
multiStmt.addStmt (select);
return multiStmt;
}
,_onOperationsDone: function (ops, resultSet)
{
if (resultSet.getError ())
{
this.operations = this.operations.concat (ops);
for (var i = 0; i < ops.length; i++)
this.operationsMap[ops[i].row.index] = ops[i];
return;
}
var isOperation = false;
resultSet.fetchResult ();
for (var i = 0; i < ops.length; i++)
{
var op = ops[i];
var row = op.row;
if (!(op.type & Db.Model.Operation.DELETE
&& op.type & Db.Model.Operation.INSERT))
isOperation = true;
if (op.type & Db.Model.Operation.DELETE)
{
resultSet.fetchResult ();
}
else if (op.type & (Db.Model.Operation.INSERT | Db.Model.Operation.UPDATE))
{
this.signalEmit ('row-updated-before', row.index);
var updatedCols = [];
var cols = this.columns;
for (var tableIndex in op.tables)
{
var j = 0;
tableIndex = parseInt (tableIndex);
resultSet.fetchResult ();
var newValues = resultSet.fetchRow ();
if (op.tables[tableIndex] & Db.Model.Operation.INSERT)
{
for (var i = 0; i < cols.length; i++)
if (cols[i].table === tableIndex)
{
row[i] = newValues[j++];
updatedCols.push (i);
}
}
else
{
for (var i = 0; i < cols.length; i++)
if (cols[i].table === tableIndex
&& op.oldValues[i] !== undefined)
{
row[i] = newValues[j++];
updatedCols.push (i);
}
}
}
this.signalEmit ('row-updated', row.index, updatedCols);
}
}
resultSet.fetchResult ();
// if (isOperation)
this.signalEmit ('operations-done');
}
/**
* Undoes all unsaved changes made to the model.
**/
,reverseOperations: function ()
{
for (var i = 0; i < this.operations.length; i++)
{
var op = this.operations[i];
var row = op.row;
if (op.type & Db.Model.Operation.DELETE
&& !(op.type & Db.Model.Operation.INSERT))
{
this.data.splice (row.index, 0, row);
this.signalEmit ('row-inserted', row.index);
}
else if (op.type & Db.Model.Operation.UPDATE)
{
this.signalEmit ('row-updated-before', row.index);
var updatedCols = [];
var cols = this.columns;
for (var i = 0; i < cols.length; i++)
if (op.oldValues[i] !== undefined)
{
row[i] = op.oldValues[i];
updatedCols.push (i);
}
this.signalEmit ('row-updated', row.index, updatedCols);
}
}
this._resetOperations ();
this._refreshRowIndexes (0);
}
,_resetOperations: function ()
{
this.operations = [];
this.operationsMap = {};
}
/*
* Function used to sort the model ascending.
*/
,sortFunctionAsc: function (column, a, b)
{
if (a[column] < b[column])
return -1;
else if (a[column] > b[column])
return 1;
return 0;
}
/*
* Function used to sort the model descending.
*/
,sortFunctionDesc: function (column, a, b)
{
if (a[column] > b[column])
return -1;
else if (a[column] < b[column])
return 1;
return 0;
}
/**
* Orders the model by the specified column.
*
* @param {integer} column The column index
* @param {Db.Model.SortWay} way The sort way
**/
,sort: function (column, way)
{
if (!this.checkColExists (column))
return;
this._setStatus (Db.Model.Status.LOADING);
if (column !== this.sortColumn)
{
if (way === Db.Model.SortWay.DESC)
var sortFunction = this.sortFunctionDesc;
else
var sortFunction = this.sortFunctionAsc;
this.data.sort (sortFunction.bind (this, column));
}
else if (way !== this.sortWay)
this.data.reverse ();
this.sortColumn = column;
this.sortWay = way;
this._refreshRowIndexes (0);
this._setStatus (Db.Model.Status.READY);
}
/**
* Builds an internal hash index for the specified column, this speeds
* significantly searches on that column, specially when model has a lot of
* rows.
*
* FIXME: Not fully implemented.
*
* @param {String} column The column name
**/
,indexColumn: function (column)
{
this.requestedIndexes[column] = true;
if (this._status === Db.Model.Status.READY)
this._buildIndex (column);
}
,_buildIndex: function (column)
{
var columnIndex = this.getColumnIndex (column);
if (columnIndex !== -1)
{
var index = {};
var data = this.data;
switch (this.columns[columnIndex].type)
{
case Db.Conn.Type.TIMESTAMP:
case Db.Conn.Type.DATE_TIME:
case Db.Conn.Type.DATE:
for (var i = 0; i < data.length; i++)
index[data[i][columnIndex].toString ()] = i;
break;
default:
for (var i = 0; i < data.length; i++)
index[data[i][columnIndex]] = i;
}
this.indexes[columnIndex] = index;
}
}
/**
* Searchs a value on the model and returns the row index of the first
* ocurrence.
* If an index have been built on that column, it will be used, for more
* information see the indexColumn() method.
*
* @param {String} column The column name
* @param {Object} value The value to search
* @return {integer} The column index
**/
,search: function (column, value)
{
var index = this.getColumnIndex (column);
return this.searchByIndex (index, value);
}
/**
* Searchs a value on the model and returns the row index of the first
* ocurrence.
*
* @param {integer} col The column index
* @param {Object} value The value to search
* @return {integer} The column index
**/
,searchByIndex: function (col, value)
{
if (!this.checkColExists (col))
return -1;
if (value)
switch (this.columns[col].type)
{
case Db.Conn.Type.BOOLEAN:
value = !!value;
break;
case Db.Conn.Type.INTEGER:
value = parseInt (value);
break;
case Db.Conn.Type.DOUBLE:
value = parseFloat (value);
break;
default:
value = value.toString ();
}
// Searchs the value using an internal index.
var index = this.indexes[col];
if (index)
{
if (index[value] !== undefined)
return index[value];
return -1;
}
// Searchs the value using a loop.
var data = this.data;
switch (this.columns[col].type)
{
case Db.Conn.Type.TIMESTAMP:
case Db.Conn.Type.DATE_TIME:
case Db.Conn.Type.DATE:
{
for (var i = 0; i < data.length; i++)
if (value === data[i][col].toString ());
return i;
break;
}
default:
for (var i = 0; i < data.length; i++)
if (value === data[i][col])
return i;
}
return -1;
}
,_setStatus: function (status)
{
this._status = status;
this.signalEmit ('status-changed', status);
this.signalEmit ('status-changed-after', status);
}
,_createTarget: function (tableIndex)
{
var table = this.tables[tableIndex];
return new Sql.Table
({
name: table.orgname
,schema: table.schema
});
}
,_createWhere: function (tableIndex, op, useOldValues)
{
var where = new Sql.Operation ({type: Sql.Operation.Type.AND});
var pks = this.tables[tableIndex].pks;
if (pks.length === 0)
return null;
for (var i = 0; i < pks.length; i++)
{
var col = pks[i];
var column = this.columns[col];
var equalOp = new Sql.Operation ({type: Sql.Operation.Type.EQUAL});
equalOp.exprs.add (new Sql.Field ({name: column.orgname}));
where.exprs.add (equalOp);
var pkValue = null;
if (useOldValues && op.oldValues
&& op.oldValues[col] !== undefined)
pkValue = op.oldValues[col];
else
pkValue = op.row[col];
if (pkValue)
equalOp.exprs.add (new Sql.Value ({value: pkValue}));
else if (column.flags & Db.Conn.Flag.AI && !useOldValues)
equalOp.exprs.add (new Sql.Func ({name: 'LAST_INSERT_ID'}));
else
return null;
}
return where;
}
,_createOperation: function (rowIndex)
{
var op = this.operationsMap[rowIndex];
if (!op)
{
op = {
type: 0,
row: this.data[rowIndex]
};
this.operations.push (op);
this.operationsMap[rowIndex] = op;
}
return op;
}
/**
* Overrides information about a table and its columns. If a parameter is
* not provided, the original will be preserved. This method should be used
* primarily to avoid the mysql bug that causes this information will not
* be set correctly.
* For more information see the following links:
* - https://bugs.mysql.com/bug.php?id=44660
* - https://bugs.mysql.com/bug.php?id=26894
*
* @param {String} table The table alias
* @param {String} orgtable The original table name
* @param {String} schema The original table schema
* @param {Array} pks Array with the names of primary keys
* @param {String} ai The autoincrement column name
**/
,setInfo: function (table, orgname, schema, pks, ai)
{
if (!this.tableInfo)
this.tableInfo = {};
this.tableInfo[table] =
{
orgname: orgname,
schema: schema,
pks: pks,
ai: ai
};
this._repairColumns ();
}
,_repairColumns: function ()
{
// Repairs wrong table info
if (this.tableInfo && this.tables)
for (var i = 0; i < this.tables.length; i++)
{
var table = this.tables[i];
var tableInfo = this.tableInfo[table.name];
if (!tableInfo)
continue;
table.orgname = tableInfo.orgname;
table.schema = tableInfo.schema;
if (tableInfo.pks)
{
table.pks = [];
for (var j = 0; j < tableInfo.pks.length; j++)
{
var colIndex = this.getColumnIndex (tableInfo.pks[j]);
table.pks.push (colIndex);
}
}
if (tableInfo.ai)
{
var colIndex = this.getColumnIndex (tableInfo.ai);
this.columns[colIndex].flags |= Db.Conn.Flag.AI;
}
}
}
});