Add a centralized, modular .save() method to DataObject

.save calls ._initSave(), _saveData(), _finalizeSave() internally passing `env` object to each to act as an environment for passing around variables
* _initSave should determine if the save is possible and return a promise for either `true` or `false`. It should also set up the environment, e.g. determine if this `isNew`
* _saveData performs the actual saving to the database, but should not do any terminal steps in the save process so that any extending classes could extend this method to write additional data to the database
* _finalizeSave should perform any finalization before the data is committed to the database.

_recoverFromSaveError is called with `env` and an error that occurred. This method should perform any recovery steps, e.g. discarding the save and reloading the item from the database into the cache.
This commit is contained in:
Aurimas Vinckevicius 2014-11-14 02:46:31 -06:00
parent 56f244a4bb
commit e02945b591
4 changed files with 872 additions and 886 deletions

View File

@ -279,168 +279,136 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) {
return objs;
}
Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this.name) {
throw new Error('Collection name is empty');
}
return Zotero.Collection._super.prototype._initSave.apply(this, arguments);
});
Zotero.Collection.prototype.save = Zotero.Promise.coroutine(function* () {
try {
Zotero.Collections.editCheck(this);
Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
var isNew = env.isNew;
var collectionID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('collections');
var libraryID = env.libraryID = this.libraryID;
var key = env.key = this._key = this.key ? this.key : this._generateKey();
Zotero.debug("Saving collection " + this.id);
// Verify parent
if (this._parentKey) {
let newParent = Zotero.Collections.getByLibraryAndKey(
this.libraryID, this._parentKey
);
if (!this.name) {
throw new Error('Collection name is empty');
if (!newParent) {
throw new Error("Cannot set parent to invalid collection " + this._parentKey);
}
if (Zotero.Utilities.isEmpty(this._changed)) {
Zotero.debug("Collection " + this.id + " has not changed");
return false;
if (newParent.id == this.id) {
throw new Error('Cannot move collection into itself!');
}
var isNew = !this.id;
// Register this item's identifiers in Zotero.DataObjects on transaction commit,
// before other callbacks run
var collectionID, libraryID, key;
if (isNew) {
var transactionOptions = {
onCommit: function () {
Zotero.Collections.registerIdentifiers(collectionID, libraryID, key);
}
};
}
else {
var transactionOptions = null;
if (this.id && (yield this.hasDescendent('collection', newParent.id))) {
throw ('Cannot move collection "' + this.name + '" into one of its own descendents');
}
return Zotero.DB.executeTransaction(function* () {
// how to know if date modified changed (in server code too?)
collectionID = this._id = this.id ? this.id : yield Zotero.ID.get('collections');
libraryID = this.libraryID;
key = this._key = this.key ? this.key : this._generateKey();
Zotero.debug("Saving collection " + this.id);
// Verify parent
if (this._parentKey) {
let newParent = Zotero.Collections.getByLibraryAndKey(
this.libraryID, this._parentKey
);
if (!newParent) {
throw new Error("Cannot set parent to invalid collection " + this._parentKey);
}
if (newParent.id == this.id) {
throw new Error('Cannot move collection into itself!');
}
if (this.id && (yield this.hasDescendent('collection', newParent.id))) {
throw ('Cannot move collection "' + this.name + '" into one of its own descendents');
}
var parent = newParent.id;
}
else {
var parent = null;
}
var columns = [
'collectionID',
'collectionName',
'parentCollectionID',
'clientDateModified',
'libraryID',
'key',
'version',
'synced'
];
var sqlValues = [
collectionID ? { int: collectionID } : null,
{ string: this.name },
parent ? parent : null,
Zotero.DB.transactionDateTime,
this.libraryID ? this.libraryID : 0,
key,
this.version ? this.version : 0,
this.synced ? 1 : 0
];
if (isNew) {
var placeholders = columns.map(function () '?').join();
var sql = "REPLACE INTO collections (" + columns.join(', ') + ") "
+ "VALUES (" + placeholders + ")";
var insertID = yield Zotero.DB.queryAsync(sql, sqlValues);
if (!collectionID) {
collectionID = insertID;
}
}
else {
columns.shift();
sqlValues.push(sqlValues.shift());
let sql = 'UPDATE collections SET '
+ columns.map(function (x) x + '=?').join(', ')
+ ' WHERE collectionID=?';
yield Zotero.DB.queryAsync(sql, sqlValues);
}
if (this._changed.parentKey) {
var parentIDs = [];
if (this.id && this._previousData.parentKey) {
parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey(
this.libraryID, this._previousData.parentKey
));
}
if (this.parentKey) {
parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey(
this.libraryID, this.parentKey
));
}
if (this.id) {
Zotero.Notifier.trigger('move', 'collection', this.id);
}
}
if (isNew && this.libraryID) {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
var group = Zotero.Groups.get(groupID);
group.clearCollectionCache();
}
if (isNew) {
Zotero.Notifier.trigger('add', 'collection', this.id);
}
else {
Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData);
}
// Invalidate cached child collections
if (parentIDs) {
Zotero.Collections.refreshChildCollections(parentIDs);
}
// New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled
if (isNew) {
var id = this.id;
this._disabled = true;
return id;
}
yield this.reload();
this._clearChanged();
return true;
}.bind(this), transactionOptions);
var parent = newParent.id;
}
catch (e) {
try {
yield this.reload();
this._clearChanged();
}
catch (e2) {
Zotero.debug(e2, 1);
}
Zotero.debug(e, 1);
throw e;
else {
var parent = null;
}
var columns = [
'collectionID',
'collectionName',
'parentCollectionID',
'clientDateModified',
'libraryID',
'key',
'version',
'synced'
];
var sqlValues = [
collectionID ? { int: collectionID } : null,
{ string: this.name },
parent ? parent : null,
Zotero.DB.transactionDateTime,
this.libraryID ? this.libraryID : 0,
key,
this.version ? this.version : 0,
this.synced ? 1 : 0
];
if (isNew) {
var placeholders = columns.map(function () '?').join();
var sql = "REPLACE INTO collections (" + columns.join(', ') + ") "
+ "VALUES (" + placeholders + ")";
var insertID = yield Zotero.DB.queryAsync(sql, sqlValues);
if (!collectionID) {
collectionID = env.id = insertID;
}
}
else {
columns.shift();
sqlValues.push(sqlValues.shift());
let sql = 'UPDATE collections SET '
+ columns.map(function (x) x + '=?').join(', ')
+ ' WHERE collectionID=?';
yield Zotero.DB.queryAsync(sql, sqlValues);
}
if (this._changed.parentKey) {
var parentIDs = [];
if (this.id && this._previousData.parentKey) {
parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey(
this.libraryID, this._previousData.parentKey
));
}
if (this.parentKey) {
parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey(
this.libraryID, this.parentKey
));
}
if (this.id) {
Zotero.Notifier.trigger('move', 'collection', this.id);
}
env.parentIDs = parentIDs;
}
});
Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
var isNew = env.isNew;
if (isNew && this.libraryID) {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
var group = Zotero.Groups.get(groupID);
group.clearCollectionCache();
}
if (isNew) {
Zotero.Notifier.trigger('add', 'collection', this.id);
}
else {
Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData);
}
// Invalidate cached child collections
if (env.parentIDs) {
Zotero.Collections.refreshChildCollections(env.parentIDs);
}
// New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled
if (isNew) {
var id = this.id;
this._disabled = true;
return id;
}
yield this.reload();
this._clearChanged();
return true;
});

View File

@ -425,6 +425,98 @@ Zotero.DataObject.prototype._clearFieldChange = function (field) {
delete this._previousData[field];
}
Zotero.DataObject.prototype.isEditable = function () {
return Zotero.Libraries.isEditable(this.libraryID);
}
Zotero.DataObject.prototype.editCheck = function () {
if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable()) {
throw ("Cannot edit " + this._objectType + " in read-only Zotero library");
}
}
/**
* Save changes to database
*
* @return {Promise<Integer|Boolean>} Promise for itemID of new item,
* TRUE on item update, or FALSE if item was unchanged
*/
Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options) {
var env = {
arguments: arguments,
transactionOptions: null,
options: options || {}
};
var proceed = yield this._initSave(env);
if (!proceed) return false;
if (env.isNew) {
Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4);
}
else {
Zotero.debug('Updating database with new ' + this._objectType + ' data', 4);
}
return Zotero.DB.executeTransaction(function* () {
yield this._saveData(env);
return yield this._finalizeSave(env);
}.bind(this), env.transactionOptions)
.catch(e => {
return this._recoverFromSaveError(env, e)
.catch(function(e2) {
Zotero.debug(e2, 1);
})
.then(function() {
Zotero.debug(e, 1);
throw e;
})
});
});
Zotero.DataObject.prototype.hasChanged = function() {
Zotero.debug(this._changed);
return !!Object.keys(this._changed).filter(dataType => this._changed[dataType]).length
}
Zotero.DataObject.prototype._saveData = function() {
throw new Error("Zotero.DataObject.prototype._saveData is an abstract method");
}
Zotero.DataObject.prototype._finalizeSave = function() {
throw new Error("Zotero.DataObject.prototype._finalizeSave is an abstract method");
}
Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* () {
yield this.reload(null, true);
this._clearChanged();
});
Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
env.isNew = !this.id;
this.editCheck();
if (!this.hasChanged()) {
Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4);
return false;
}
// Register this object's identifiers in Zotero.DataObjects on transaction commit,
// before other callbacks run
if (env.isNew) {
env.transactionOptions = {
onCommit: () => {
this.ObjectsClass.registerIdentifiers(env.id, env.libraryID, env.key);
}
};
}
return true;
});
/**
* Generates data object key
* @return {String} key

File diff suppressed because it is too large Load Diff

View File

@ -173,6 +173,13 @@ Zotero.Search.prototype.loadFromRow = function (row) {
this._identified = true;
}
Zotero.Search.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this.name) {
throw('Name not provided for saved search');
}
return Zotero.Search._super.prototype._initSave.apply(this, arguments);
});
/*
* Save the search to the DB and return a savedSearchID
@ -183,142 +190,107 @@ Zotero.Search.prototype.loadFromRow = function (row) {
*
* For new searches, name must be set called before saving
*/
Zotero.Search.prototype.save = Zotero.Promise.coroutine(function* (fixGaps) {
try {
Zotero.Searches.editCheck(this);
if (!this.name) {
throw('Name not provided for saved search');
}
var isNew = !this.id;
// Register this item's identifiers in Zotero.DataObjects on transaction commit,
// before other callbacks run
var searchID, libraryID, key;
if (isNew) {
var transactionOptions = {
onCommit: function () {
Zotero.Searches.registerIdentifiers(searchID, libraryID, key);
}
};
}
else {
var transactionOptions = null;
}
return Zotero.DB.executeTransaction(function* () {
searchID = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches');
libraryID = this.libraryID;
key = this._key = this.key ? this.key : this._generateKey();
Zotero.debug("Saving " + (isNew ? 'new ' : '') + "search " + this.id);
var columns = [
'savedSearchID',
'savedSearchName',
'clientDateModified',
'libraryID',
'key',
'version',
'synced'
];
var placeholders = columns.map(function () '?').join();
var sqlValues = [
searchID ? { int: searchID } : null,
{ string: this.name },
Zotero.DB.transactionDateTime,
this.libraryID ? this.libraryID : 0,
key,
this.version ? this.version : 0,
this.synced ? 1 : 0
];
var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") "
+ "VALUES (" + placeholders + ")";
var insertID = yield Zotero.DB.queryAsync(sql, sqlValues);
if (!searchID) {
searchID = insertID;
}
if (!isNew) {
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
yield Zotero.DB.queryAsync(sql, this.id);
}
// Close gaps in savedSearchIDs
var saveConditions = {};
var i = 1;
for (var id in this._conditions) {
if (!fixGaps && id != i) {
Zotero.DB.rollbackTransaction();
throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id);
}
saveConditions[i] = this._conditions[id];
i++;
}
this._conditions = saveConditions;
for (var i in this._conditions){
var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
+ "searchConditionID, condition, operator, value, required) "
+ "VALUES (?,?,?,?,?,?)";
// Convert condition and mode to "condition[/mode]"
var condition = this._conditions[i].mode ?
this._conditions[i].condition + '/' + this._conditions[i].mode :
this._conditions[i].condition
var sqlParams = [
searchID,
i,
condition,
this._conditions[i].operator ? this._conditions[i].operator : null,
this._conditions[i].value ? this._conditions[i].value : null,
this._conditions[i].required ? 1 : null
];
yield Zotero.DB.queryAsync(sql, sqlParams);
}
if (isNew) {
Zotero.Notifier.trigger('add', 'search', this.id);
}
else {
Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData);
}
if (isNew && this.libraryID) {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
var group = yield Zotero.Groups.get(groupID);
group.clearSearchCache();
}
if (isNew) {
var id = this.id;
this._disabled = true;
return id;
}
yield this.reload();
this._clearChanged();
return isNew ? this.id : true;
}.bind(this), transactionOptions);
Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
var fixGaps = env.arguments[0];
var isNew = env.isNew;
var searchID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches');
var libraryID = env.libraryID = this.libraryID;
var key = env.key = this._key = this.key ? this.key : this._generateKey();
var columns = [
'savedSearchID',
'savedSearchName',
'clientDateModified',
'libraryID',
'key',
'version',
'synced'
];
var placeholders = columns.map(function () '?').join();
var sqlValues = [
searchID ? { int: searchID } : null,
{ string: this.name },
Zotero.DB.transactionDateTime,
this.libraryID ? this.libraryID : 0,
key,
this.version ? this.version : 0,
this.synced ? 1 : 0
];
var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") "
+ "VALUES (" + placeholders + ")";
var insertID = yield Zotero.DB.queryAsync(sql, sqlValues);
if (!searchID) {
searchID = env.id = insertID;
}
catch (e) {
try {
yield this.reload();
this._clearChanged();
}
catch (e2) {
Zotero.debug(e2, 1);
}
Zotero.debug(e, 1);
throw e;
if (!isNew) {
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
yield Zotero.DB.queryAsync(sql, this.id);
}
// Close gaps in savedSearchIDs
var saveConditions = {};
var i = 1;
for (var id in this._conditions) {
if (!fixGaps && id != i) {
Zotero.DB.rollbackTransaction();
throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id);
}
saveConditions[i] = this._conditions[id];
i++;
}
this._conditions = saveConditions;
for (var i in this._conditions){
var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
+ "searchConditionID, condition, operator, value, required) "
+ "VALUES (?,?,?,?,?,?)";
// Convert condition and mode to "condition[/mode]"
var condition = this._conditions[i].mode ?
this._conditions[i].condition + '/' + this._conditions[i].mode :
this._conditions[i].condition
var sqlParams = [
searchID,
i,
condition,
this._conditions[i].operator ? this._conditions[i].operator : null,
this._conditions[i].value ? this._conditions[i].value : null,
this._conditions[i].required ? 1 : null
];
yield Zotero.DB.queryAsync(sql, sqlParams);
}
});
Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
var isNew = env.isNew;
if (isNew) {
Zotero.Notifier.trigger('add', 'search', this.id);
}
else {
Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData);
}
if (isNew && this.libraryID) {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
var group = yield Zotero.Groups.get(groupID);
group.clearSearchCache();
}
if (isNew) {
var id = this.id;
this._disabled = true;
return id;
}
yield this.reload();
this._clearChanged();
return isNew ? this.id : true;
});