Skip to content

Commit

Permalink
Add transactions for import
Browse files Browse the repository at this point in the history
closes #837
- added transaction handling for import
- added transactions to model functions
- added simple tests for failing imports
  • Loading branch information
sebgie committed Nov 20, 2013
1 parent 1c5a811 commit 77ed7f8
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 62 deletions.
100 changes: 62 additions & 38 deletions core/server/data/import/000.js
Expand Up @@ -78,32 +78,32 @@ function preProcessPostTags(tableData) {
return tableData;
}

function importTags(ops, tableData) {
function importTags(ops, tableData, transaction) {
tableData = stripProperties(['id'], tableData);
_.each(tableData, function (tag) {
ops.push(models.Tag.read({name: tag.name}).then(function (_tag) {
ops.push(models.Tag.findOne({name: tag.name}, {transacting: transaction}).then(function (_tag) {
if (!_tag) {
return models.Tag.add(tag);
return models.Tag.add(tag, {transacting: transaction});
}
return when.resolve(_tag);
}));
});
}

function importPosts(ops, tableData) {
function importPosts(ops, tableData, transaction) {
tableData = stripProperties(['id'], tableData);
_.each(tableData, function (post) {
ops.push(models.Post.add(post));
ops.push(models.Post.add(post, {transacting: transaction}));
});
}

function importUsers(ops, tableData) {
function importUsers(ops, tableData, transaction) {
tableData = stripProperties(['id'], tableData);
tableData[0].id = 1;
ops.push(models.User.edit(tableData[0]));
ops.push(models.User.edit(tableData[0], {transacting: transaction}));
}

function importSettings(ops, tableData) {
function importSettings(ops, tableData, transaction) {
// for settings we need to update individual settings, and insert any missing ones
// the one setting we MUST NOT update is the databaseVersion settings
var blackList = ['databaseVersion'];
Expand All @@ -112,48 +112,72 @@ function importSettings(ops, tableData) {
return blackList.indexOf(data.key) === -1;
});

ops.push(models.Settings.edit(tableData));
ops.push(models.Settings.edit(tableData, transaction));
}

// No data needs modifying, we just import whatever tables are available
Importer000.prototype.basicImport = function (data) {
var ops = [],
tableData = data.data;
return models.Base.transaction(function (t) {

// Do any pre-processing of relationships (we can't depend on ids)
if (tableData.posts_tags && tableData.posts && tableData.tags) {
tableData = preProcessPostTags(tableData);
}
// Do any pre-processing of relationships (we can't depend on ids)
if (tableData.posts_tags && tableData.posts && tableData.tags) {
tableData = preProcessPostTags(tableData);
}

// Import things in the right order:
if (tableData.tags && tableData.tags.length) {
importTags(ops, tableData.tags);
}
// Import things in the right order:
if (tableData.tags && tableData.tags.length) {
importTags(ops, tableData.tags, t);
}

if (tableData.posts && tableData.posts.length) {
importPosts(ops, tableData.posts);
}
if (tableData.posts && tableData.posts.length) {
importPosts(ops, tableData.posts, t);
}

if (tableData.users && tableData.users.length) {
importUsers(ops, tableData.users);
}
if (tableData.users && tableData.users.length) {
importUsers(ops, tableData.users, t);
}

if (tableData.settings && tableData.settings.length) {
importSettings(ops, tableData.settings);
}
if (tableData.settings && tableData.settings.length) {
importSettings(ops, tableData.settings, t);
}

/** do nothing with these tables, the data shouldn't have changed from the fixtures
* permissions
* roles
* permissions_roles
* permissions_users
* roles_users
*/

return when.all(ops).then(function (results) {
return when.resolve(results);
}, function (err) {
return when.reject("Error importing data: " + err.message || err, err.stack);
/** do nothing with these tables, the data shouldn't have changed from the fixtures
* permissions
* roles
* permissions_roles
* permissions_users
* roles_users
*/

// Write changes to DB, if successful commit, otherwise rollback
// when.all() does not work as expected, when.settle() does.
when.settle(ops).then(function (descriptors) {
var rej = false,
error = '';
descriptors.forEach(function (d) {
if (d.state === 'rejected') {
error += _.isEmpty(error) ? '' : '</br>';
if (!_.isEmpty(d.reason.clientError)) {
error += d.reason.clientError;
} else if (!_.isEmpty(d.reason.message)) {
error += d.reason.message;
}
rej = true;
}
});
if (rej) {
t.rollback(error);
} else {
t.commit();
}
});
}).then(function () {
//TODO: could return statistics of imported items
return when.resolve();
}, function (error) {
return when.reject("Error importing data: " + error);
});
};

Expand Down
12 changes: 8 additions & 4 deletions core/server/models/base.js
Expand Up @@ -90,8 +90,12 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
slugTryCount = 1,
// Look for a post with a matching slug, append an incrementing number if so
checkIfSlugExists = function (slugToFind) {
readOptions = _.extend(readOptions || {}, { slug: slugToFind });
return Model.read(readOptions).then(function (found) {
var args = {slug: slugToFind};
//status is needed for posts
if (readOptions && readOptions.status) {
args.status = readOptions.status;
}
return Model.findOne(args, readOptions).then(function (found) {
var trimSpace;

if (!found) {
Expand Down Expand Up @@ -177,7 +181,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
edit: function (editedObj, options) {
options = options || {};
return this.forge({id: editedObj.id}).fetch(options).then(function (foundObj) {
return foundObj.save(editedObj);
return foundObj.save(editedObj, options);
});
},

Expand All @@ -192,7 +196,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
*/
add: function (newObj, options) {
options = options || {};
return this.forge(newObj).save(options);
return this.forge(newObj).save(null, options);
},

create: function () {
Expand Down
1 change: 1 addition & 0 deletions core/server/models/index.js
Expand Up @@ -7,6 +7,7 @@ module.exports = {
Permission: require('./permission').Permission,
Settings: require('./settings').Settings,
Tag: require('./tag').Tag,
Base: require('./base'),
init: function () {
return migrations.init();
},
Expand Down
44 changes: 28 additions & 16 deletions core/server/models/post.js
Expand Up @@ -39,11 +39,12 @@ Post = ghostBookshelf.Model.extend({

validate: function () {
ghostBookshelf.validator.check(this.get('title'), "Post title cannot be blank").notEmpty();

ghostBookshelf.validator.check(this.get('title'), 'Post title maximum length is 150 characters.').len(0, 150);
return true;
},

saving: function () {
saving: function (newPage, attr, options) {
/*jslint unparam:true*/
var self = this;

// Remove any properties which don't belong on the post model
Expand All @@ -65,14 +66,15 @@ Post = ghostBookshelf.Model.extend({

if (this.hasChanged('slug')) {
// Pass the new slug through the generator to strip illegal characters, detect duplicates
return this.generateSlug(Post, this.get('slug'), { status: 'all' })
return this.generateSlug(Post, this.get('slug'), {status: 'all', transacting: options.transacting})
.then(function (slug) {
self.set({slug: slug});
});
}
},

creating: function () {
creating: function (newPage, attr, options) {
/*jslint unparam:true*/
// set any dynamic default properties
var self = this;

Expand All @@ -84,15 +86,17 @@ Post = ghostBookshelf.Model.extend({

if (!this.get('slug')) {
// Generating a slug requires a db call to look for conflicting slugs
return this.generateSlug(Post, this.get('title'), { status: 'all' })
return this.generateSlug(Post, this.get('title'), {status: 'all', transacting: options.transacting})
.then(function (slug) {
self.set({slug: slug});
});
}
},

updateTags: function (newTags) {
updateTags: function (newTags, attr, options) {
/*jslint unparam:true*/
var self = this;
options = options || {};


if (newTags === this) {
Expand All @@ -103,7 +107,8 @@ Post = ghostBookshelf.Model.extend({
return;
}

return Post.forge({id: this.id}).fetch({withRelated: ['tags']}).then(function (thisPostWithTags) {
return Post.forge({id: this.id}).fetch({withRelated: ['tags'], transacting: options.transacting}).then(function (thisPostWithTags) {

var existingTags = thisPostWithTags.related('tags').toJSON(),
tagOperations = [],
tagsToDetach = [],
Expand All @@ -117,7 +122,7 @@ Post = ghostBookshelf.Model.extend({
});

if (tagsToDetach.length > 0) {
tagOperations.push(self.tags().detach(tagsToDetach));
tagOperations.push(self.tags().detach(tagsToDetach, options));
}

// Next check if new tags are all exactly the same as what is set on the model
Expand All @@ -129,17 +134,22 @@ Post = ghostBookshelf.Model.extend({
});

if (!_.isEmpty(tagsToAttach)) {
return Tags.forge().query('whereIn', 'name', _.pluck(tagsToAttach, 'name')).fetch().then(function (matchingTags) {
return Tags.forge().query('whereIn', 'name', _.pluck(tagsToAttach, 'name')).fetch(options).then(function (matchingTags) {
_.each(matchingTags.toJSON(), function (matchingTag) {
tagOperations.push(self.tags().attach(matchingTag.id));
tagOperations.push(self.tags().attach(matchingTag.id, options));
tagsToAttach = _.reject(tagsToAttach, function (tagToAttach) {
return tagToAttach.name === matchingTag.name;
});
});

_.each(tagsToAttach, function (tagToCreateAndAttach) {
var createAndAttachOperation = Tag.add({name: tagToCreateAndAttach.name}).then(function (createdTag) {
return self.tags().attach(createdTag.id, createdTag.name);
var createAndAttachOperation,
opt = options.method;
//TODO: remove when refactor; ugly fix to overcome bookshelf
options.method = 'insert';
createAndAttachOperation = Tag.add({name: tagToCreateAndAttach.name}, options).then(function (createdTag) {
options.method = opt;
return self.tags().attach(createdTag.id, createdTag.name, options);
});


Expand Down Expand Up @@ -339,21 +349,23 @@ Post = ghostBookshelf.Model.extend({
// Otherwise, you shall not pass.
return when.reject();
},

add: function (newPostData, options) {
var self = this;
return ghostBookshelf.Model.add.call(this, newPostData, options).then(function (post) {
// associated models can't be created until the post has an ID, so run this after
return when(post.updateTags(newPostData.tags)).then(function () {
return self.findOne({status: 'all', id: post.id});
return when(post.updateTags(newPostData.tags, null, options)).then(function () {
return self.findOne({status: 'all', id: post.id}, options);
});
});
},
edit: function (editedPost, options) {
var self = this;

return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (editedObj) {
return self.findOne({status: 'all', id: editedObj.id});
return when(editedObj.updateTags(editedPost.tags, null, options)).then(function () {
return self.findOne({status: 'all', id: editedObj.id}, options);
});
//return self.findOne({status: 'all', id: editedObj.id}, options);
});
},
destroy: function (_identifier, options) {
Expand Down
9 changes: 5 additions & 4 deletions core/server/models/settings.js
Expand Up @@ -95,19 +95,20 @@ Settings = ghostBookshelf.Model.extend({
return ghostBookshelf.Model.read.call(this, _key);
},

edit: function (_data) {
edit: function (_data, t) {
var settings = this;
if (!Array.isArray(_data)) {
_data = [_data];
}
return when.map(_data, function (item) {
// Accept an array of models as input
if (item.toJSON) { item = item.toJSON(); }
return settings.forge({ key: item.key }).fetch().then(function (setting) {
return settings.forge({ key: item.key }).fetch({transacting: t}).then(function (setting) {

if (setting) {
return setting.set('value', item.value).save();
return setting.set('value', item.value).save(null, {transacting: t});
}
return settings.forge({ key: item.key, value: item.value }).save();
return settings.forge({ key: item.key, value: item.value }).save(null, {transacting: t});

}, errors.logAndThrowError);
});
Expand Down

0 comments on commit 77ed7f8

Please sign in to comment.