726364d091
Partially integrated into data layer, but I'm waiting to commit that part until I'm sure it won't break everything
429 lines
11 KiB
JavaScript
429 lines
11 KiB
JavaScript
Scholar.History = new function(){
|
|
this.begin = begin;
|
|
this.add = add;
|
|
this.modify = modify;
|
|
this.remove = remove;
|
|
this.commit = commit;
|
|
this.cancel = cancel;
|
|
this.getPreviousEvent = getPreviousEvent;
|
|
this.getNextEvent = getNextEvent;
|
|
this.undo = undo;
|
|
this.redo = redo;
|
|
this.clear = clear;
|
|
this.clearAfter = clearAfter;
|
|
|
|
var _firstTime = true;
|
|
var _currentID = 0;
|
|
var _activeID;
|
|
var _activeEvent;
|
|
var _maxID = 0;
|
|
|
|
// event: ('item-add', 'item-delete', 'item-modify', 'collection-add', 'collection-modify', 'collection-delete')
|
|
// context: (itemCreators.itemID-creatorID.1-1)
|
|
// action: ('add', 'delete', 'modify')
|
|
|
|
/**
|
|
* Begin a transaction set
|
|
**/
|
|
function begin(event, id){
|
|
if (_activeID){
|
|
throw('History transaction set already in progress');
|
|
}
|
|
|
|
// If running for the first time this session or we're in the middle of
|
|
// the history, clear any transaction sets after the current position
|
|
if (_firstTime || _currentID<_maxID){
|
|
_firstTime = false;
|
|
this.clearAfter();
|
|
}
|
|
|
|
Scholar.debug('Beginning history transaction set ' + event);
|
|
var sql = "INSERT INTO transactionSets (event, id) VALUES "
|
|
+ "('" + event + "', ";
|
|
// If integer, insert natively; if array, insert as string
|
|
sql += (typeof id=='object') ? "'" + id.join('-') + "'" : id;
|
|
sql += ")";
|
|
|
|
Scholar.DB.beginTransaction();
|
|
_activeID = Scholar.DB.query(sql);
|
|
_activeEvent = event;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add an add transaction to the current set
|
|
**/
|
|
function add(table, key, keyValues){
|
|
return _addTransaction('add', table, key, keyValues);
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a modify transaction to the current set
|
|
*
|
|
* _field_ is optional -- otherwise all fields are saved
|
|
**/
|
|
function modify(table, key, keyValues, field){
|
|
return _addTransaction('modify', table, key, keyValues, field);
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a remove transaction to the current set
|
|
**/
|
|
function remove(table, key, keyValues){
|
|
return _addTransaction('remove', table, key, keyValues);
|
|
}
|
|
|
|
|
|
/**
|
|
* Commit the current transaction set
|
|
**/
|
|
function commit(){
|
|
Scholar.debug('Committing history transaction set ' + _activeEvent);
|
|
Scholar.DB.commitTransaction();
|
|
_currentID = _activeID;
|
|
_maxID = _activeID;
|
|
_activeID = null;
|
|
_activeEvent = null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Cancel the current transaction set
|
|
**/
|
|
function cancel(){
|
|
Scholar.debug('Cancelling history transaction set ' + _activeEvent);
|
|
Scholar.DB.rollbackTransaction();
|
|
_activeID = null;
|
|
_activeEvent = null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the next event to undo, or false if none
|
|
**/
|
|
function getPreviousEvent(){
|
|
if (!_currentID){
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
|
|
+ _currentID;
|
|
return Scholar.DB.valueQuery(sql);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the next event to redo, or false if none
|
|
**/
|
|
function getNextEvent(){
|
|
var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
|
|
+ (_currentID + 1);
|
|
return Scholar.DB.valueQuery(sql);
|
|
}
|
|
|
|
|
|
/**
|
|
* Undo the last transaction set
|
|
**/
|
|
function undo(){
|
|
if (!_currentID){
|
|
throw('No transaction set to undo');
|
|
return false;
|
|
}
|
|
|
|
var id = _currentID;
|
|
Scholar.debug('Undoing transaction set ' + id);
|
|
Scholar.DB.beginTransaction();
|
|
var undone = _do('undo');
|
|
_currentID--;
|
|
Scholar.DB.commitTransaction();
|
|
_notifyEvent(id);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Redo the next transaction set
|
|
**/
|
|
function redo(){
|
|
var id = _currentID + 1;
|
|
Scholar.debug('Redoing transaction set ' + id);
|
|
Scholar.DB.beginTransaction();
|
|
var redone = _do('redo');
|
|
_currentID++;
|
|
Scholar.DB.commitTransaction();
|
|
_notifyEvent(id);
|
|
return redone;
|
|
}
|
|
|
|
|
|
/**
|
|
* Clear the entire history
|
|
**/
|
|
function clear(){
|
|
Scholar.DB.beginTransaction();
|
|
Scholar.DB.query("DELETE FROM transactionSets");
|
|
Scholar.DB.query("DELETE FROM transactions");
|
|
Scholar.DB.query("DELETE FROM transactionLog");
|
|
_currentID = null;
|
|
_activeID = null;
|
|
_activeEvent = null;
|
|
_maxID = null;
|
|
Scholar.DB.commitTransaction();
|
|
}
|
|
|
|
|
|
/**
|
|
* Clear all transactions in history after the current one
|
|
**/
|
|
function clearAfter(){
|
|
Scholar.DB.beginTransaction();
|
|
var min = Scholar.DB.valueQuery("SELECT MIN(transactionID) FROM "
|
|
+ "transactions WHERE transactionSetID=" + (_currentID + 1));
|
|
|
|
if (!min){
|
|
Scholar.DB.rollbackTransaction();
|
|
return;
|
|
}
|
|
|
|
Scholar.DB.query("DELETE FROM transactionLog "
|
|
+ "WHERE transactionID>=" + min);
|
|
Scholar.DB.query("DELETE FROM transactions "
|
|
+ "WHERE transactionID>=" + min);
|
|
Scholar.DB.query("DELETE FROM transactionSets "
|
|
+ "WHERE transactionSetID>" + _currentID);
|
|
|
|
_maxID = _currentID;
|
|
_activeID = null;
|
|
Scholar.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
|
|
//
|
|
// Private methods
|
|
//
|
|
|
|
function _addTransaction(action, table, key, keyValues, field){
|
|
if (!_activeID){
|
|
throw('Cannot add history transaction with no transaction set in progress');
|
|
}
|
|
|
|
if (typeof keyValues == 'object'){
|
|
keyValues = keyValues.join('-');
|
|
}
|
|
|
|
var contextString = table + '.' + key + '.' + keyValues;
|
|
var context = _parseContext(contextString);
|
|
var fromClause = _contextToSQLFrom(context);
|
|
|
|
var sql = "INSERT INTO transactions (transactionSetID, context, action) "
|
|
+ "VALUES (" + _activeID + ", '" + contextString
|
|
+ "', '" + action + "')";
|
|
|
|
var transactionID = Scholar.DB.query(sql);
|
|
|
|
switch (action){
|
|
case 'add':
|
|
// No need to store an add, since we'll just delete it to reverse
|
|
break;
|
|
case 'modify':
|
|
// Only save one field -- _do() won't know about this, but the
|
|
// UPDATE statements on the other fields just won't do anything
|
|
if (field){
|
|
var sql = "INSERT INTO transactionLog SELECT " + transactionID
|
|
+ ", '" + field + "', " + field + fromClause;
|
|
Scholar.DB.query(sql);
|
|
break;
|
|
}
|
|
// Fall through if no field specified and save all
|
|
case 'remove':
|
|
var cols = Scholar.DB.getColumns(table);
|
|
for (var i in cols){
|
|
// If column is not part of the key, log it
|
|
if (!Scholar.inArray(cols[i], context['keys'])){
|
|
var sql = "INSERT INTO transactionLog "
|
|
+ "SELECT " + transactionID + ", '" + cols[i]
|
|
+ "', " + cols[i] + fromClause;
|
|
Scholar.DB.query(sql);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
Scholar.DB.rollbackTransaction();
|
|
throw("Invalid history action '" + action + "'");
|
|
}
|
|
}
|
|
|
|
|
|
function _do(mode){
|
|
switch (mode){
|
|
case 'undo':
|
|
var id = _currentID;
|
|
break;
|
|
case 'redo':
|
|
var id = _currentID + 1;
|
|
break;
|
|
}
|
|
|
|
var sql = "SELECT transactionID, context, action FROM transactions "
|
|
+ "WHERE transactionSetID=" + id;
|
|
var transactions = Scholar.DB.query(sql);
|
|
|
|
if (!transactions){
|
|
throw('Transaction set not found for '
|
|
+ (mode=='undo' ? 'current' : 'next') + id);
|
|
}
|
|
|
|
for (var i in transactions){
|
|
var transactionID = transactions[i]['transactionID'];
|
|
var context = _parseContext(transactions[i]['context']);
|
|
|
|
// If in redo mode, swap 'add' and 'remove'
|
|
if (mode=='redo'){
|
|
switch (transactions[i]['action']){
|
|
case 'add':
|
|
transactions[i]['action'] = 'remove';
|
|
break;
|
|
case 'remove':
|
|
transactions[i]['action'] = 'add';
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (transactions[i]['action']){
|
|
case 'add':
|
|
var fromClause = _contextToSQLFrom(context);
|
|
|
|
// First, store the row we're about to delete for later redo
|
|
var cols = Scholar.DB.getColumns(context['table']);
|
|
for (var i in cols){
|
|
// If column is not part of the key, log it
|
|
if (!Scholar.inArray(cols[i], context['keys'])){
|
|
var sql = "INSERT INTO transactionLog "
|
|
+ "SELECT " + transactionID + ", '" + cols[i]
|
|
+ "', " + cols[i] + fromClause;
|
|
Scholar.DB.query(sql);
|
|
}
|
|
}
|
|
|
|
// And delete the row
|
|
var sql = "DELETE" + fromClause;
|
|
Scholar.DB.query(sql);
|
|
break;
|
|
|
|
case 'modify':
|
|
// Retrieve old values
|
|
var sql = "SELECT field, value FROM transactionLog "
|
|
+ "WHERE transactionID=" + transactionID;
|
|
var oldFieldValues = Scholar.DB.query(sql);
|
|
|
|
// Retrieve new values
|
|
var sql = "SELECT *" + _contextToSQLFrom(context);
|
|
var newValues = Scholar.DB.rowQuery(sql);
|
|
|
|
// Update row with old values
|
|
var sql = "UPDATE " + context['table'] + " SET ";
|
|
var values = [];
|
|
for (var i in oldFieldValues){
|
|
sql += oldFieldValues[i]['field'] + '=?, ';
|
|
values.push(oldFieldValues[i]['value']);
|
|
}
|
|
sql = sql.substr(0, sql.length-2) + _contextToSQLWhere(context);
|
|
Scholar.DB.query(sql, values);
|
|
|
|
// Update log with new values for later redo
|
|
for (var i in newValues){
|
|
if (!Scholar.inArray(i, context['keys'])){
|
|
var sql = "UPDATE transactionLog SET "
|
|
+ "value=? WHERE transactionID=? AND field=?";
|
|
Scholar.DB.query(sql, [i, newValues[i], transactionID]);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'remove':
|
|
// Retrieve old values
|
|
var sql = "SELECT field, value FROM transactionLog "
|
|
+ "WHERE transactionID=" + transactionID;
|
|
var oldFieldValues = Scholar.DB.query(sql);
|
|
|
|
// Add key to parameters
|
|
var fields = [], values = [], marks = [];
|
|
for (var i=0; i<context['keys'].length; i++){
|
|
fields.push(context['keys'][i]);
|
|
values.push(context['values'][i]);
|
|
marks.push('?');
|
|
}
|
|
|
|
// Add other fields to parameters
|
|
for (var i in oldFieldValues){
|
|
fields.push(oldFieldValues[i]['field']);
|
|
values.push(oldFieldValues[i]['value']);
|
|
marks.push('?');
|
|
}
|
|
|
|
// Insert old values into table
|
|
var sql = "INSERT INTO " + context['table'] + "("
|
|
+ fields.join() + ") VALUES (" + marks.join() + ")";
|
|
Scholar.DB.query(sql, values);
|
|
|
|
// Delete restored data from transactionLog
|
|
var sql = "DELETE FROM transactionLog WHERE transactionID="
|
|
+ transactionID;
|
|
Scholar.DB.query(sql);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function _parseContext(context){
|
|
var parts = context.split('.');
|
|
var parsed = {
|
|
table:parts[0],
|
|
keys:parts[1].split('-'),
|
|
values:parts[2].split('-')
|
|
}
|
|
if (parsed['keys'].length!=parsed['values'].length){
|
|
throw("Different number of keys and values in _parseContext('"
|
|
+ context + "')");
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
|
|
function _contextToSQLFrom(parsed){
|
|
return " FROM " + parsed['table'] + _contextToSQLWhere(parsed);
|
|
}
|
|
|
|
|
|
function _contextToSQLWhere(parsed){
|
|
var sql = " WHERE ";
|
|
for (var i=0; i<parsed['keys'].length; i++){
|
|
// DEBUG: type?
|
|
sql += parsed['keys'][i] + "='" + parsed['values'][i] + "' AND ";
|
|
}
|
|
return sql.substr(0, sql.length-5);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the ids associated with a particular transaction set
|
|
**/
|
|
function _getSetData(transactionSetID){
|
|
var sql = "SELECT event, id FROM transactionSets WHERE transactionSetID="
|
|
+ transactionSetID;
|
|
return Scholar.DB.rowQuery(sql);
|
|
}
|
|
|
|
|
|
function _notifyEvent(transactionSetID){
|
|
var data = _getSetData(transactionSetID);
|
|
var eventParts = data['event'].split('-'); // e.g. modify-item
|
|
Scholar.Notifier.trigger(eventParts[0], eventParts[1], data['id']);
|
|
}
|
|
}
|