🐛 Fixed importer bug: can't resolve authors relation

closes #9547

- you setup a blog with the following owner:
  - email: test@ghost.org
  - name: test
  - slug: test
- now you import a JSON db file, which holds the exact same owner
  - this owner won't be imported, because it's a duplicate
  - but the slug is different (!)
- the importer tries to find a matching existing user, but won't find anything
- the importer then send an empty authors array `post.authors=[]` into the model layer
- this is not allowed -> this would mean, you are actively trying to unset all authors
This commit is contained in:
kirrg001 2018-04-10 00:20:19 +02:00
parent 87501fd41f
commit d209a4d013
5 changed files with 98 additions and 12 deletions

View File

@ -74,6 +74,12 @@ class PostsImporter extends BaseImporter {
* Replace all identifier references.
*/
replaceIdentifiers() {
const ownerUserId = _.find(this.requiredExistingData.users, (user) => {
if (user.roles[0].name === 'Owner') {
return true;
}
}).id;
const run = (postToImport, postIndex, targetProperty, tableName) => {
if (!postToImport[targetProperty] || !postToImport[targetProperty].length) {
return;
@ -104,7 +110,7 @@ class PostsImporter extends BaseImporter {
return;
}
// CASE: search through existing data
// CASE: search through existing data by unique attribute
let existingObject = _.find(this.requiredExistingData[tableName], {slug: objectInFile.slug});
if (existingObject) {
@ -117,6 +123,17 @@ class PostsImporter extends BaseImporter {
this.dataToImport[postIndex][targetProperty] = _.filter(this.dataToImport[postIndex][targetProperty], ((object, index) => {
return indexesToRemove.indexOf(index) === -1;
}));
// CASE: we had to remove all the relations, because we could not match or find the relation reference.
// e.g. you import a post with multiple authors. Only the primary author is assigned.
// But the primary author won't be imported and we can't find the author in the existing database.
// This would end in `post.authors = []`, which is not allowed. There must be always minimum one author.
// We fallback to the owner user.
if (targetProperty === 'authors' && !this.dataToImport[postIndex][targetProperty].length) {
this.dataToImport[postIndex][targetProperty] = [{
id: ownerUserId
}];
}
};
_.each(this.dataToImport, (postToImport, postIndex) => {

View File

@ -105,6 +105,13 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
*/
model.unset('author');
// CASE: you can't delete all authors
if (model.get('authors') && !model.get('authors').length) {
throw new common.errors.ValidationError({
message: 'At least one author is required.'
});
}
// CASE: `post.author_id` has changed
if (model.hasChanged('author_id')) {
// CASE: you don't send `post.authors`
@ -126,13 +133,6 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
}
}
// CASE: you can't delete all authors
if (model.get('authors') && !model.get('authors').length) {
throw new common.errors.ValidationError({
message: 'At least one author is required.'
});
}
// CASE: if you change `post.author_id`, we have to update the primary author
// CASE: if the `author_id` has change and you pass `posts.authors`, we already check above that
// the primary author id must be equal

View File

@ -1858,7 +1858,7 @@ describe('Import (new test structure)', function () {
const posts = importedData[0].posts,
users = importedData[1].users;
// owner + 3 imported users
// owner + 3 imported users + 1 duplicate
users.length.should.eql(4);
users[0].slug.should.eql(exportData.data.users[0].slug);
users[1].slug.should.eql(exportData.data.users[1].slug);
@ -1872,9 +1872,10 @@ describe('Import (new test structure)', function () {
posts[0].authors.length.should.eql(1);
posts[0].authors[0].id.should.eql(users[2].id);
posts[1].author.should.eql(users[0].id);
// falls back to owner user, because target reference could not be resolved (duplicate)
posts[1].author.should.eql(testUtils.DataGenerator.Content.users[0].id);
posts[1].authors.length.should.eql(1);
posts[1].authors[0].id.should.eql(users[0].id);
posts[1].authors[0].id.should.eql(testUtils.DataGenerator.Content.users[0].id);
posts[2].author.should.eql(users[1].id);
posts[2].authors.length.should.eql(3);

View File

@ -281,6 +281,34 @@ describe('Unit: models/post', function () {
});
});
it('[not allowed] with empty authors ([]), without author_id', function () {
const post = testUtils.DataGenerator.forKnex.createPost();
delete post.author_id;
post.authors = [];
return models.Post.add(post, {withRelated: ['author', 'authors']})
.then(function () {
'Expected error'.should.eql(false);
})
.catch(function (err) {
(err instanceof common.errors.ValidationError).should.eql(true);
});
});
it('[not allowed] with empty authors ([]), with author_id', function () {
const post = testUtils.DataGenerator.forKnex.createPost();
post.author_id.should.eql(testUtils.DataGenerator.forKnex.users[0].id);
post.authors = [];
return models.Post.add(post, {withRelated: ['author', 'authors']})
.then(function () {
'Expected error'.should.eql(false);
})
.catch(function (err) {
(err instanceof common.errors.ValidationError).should.eql(true);
});
});
it('with authors, with author_id', function () {
const post = testUtils.DataGenerator.forKnex.createPost();
post.author_id.should.eql(testUtils.DataGenerator.forKnex.users[0].id);

View File

@ -247,6 +247,34 @@
"created_by": "1",
"updated_at": "2017-09-01T12:30:59.000Z",
"updated_by": "1"
},
{
"id": "5951f5fca366002ebd5dbef11",
"name": "Joe Blogg's",
"slug": "joe-bloggs-1",
"ghost_auth_access_token": null,
"ghost_auth_id": null,
"password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC",
"email": "jbloggs@example.com",
"profile_image": "/content/images/2017/05/authorlogo.jpeg",
"cover_image": "/content/images/2017/05/authorcover.jpeg",
"bio": "I'm Joe's father, the good looking one!",
"website": "http://joebloggs.com",
"location": null,
"facebook": null,
"twitter": null,
"accessibility": null,
"status": "active",
"locale": "en_US",
"visibility": "public",
"meta_title": null,
"meta_description": null,
"tour": "[\"getting-started\",\"using-the-editor\",\"static-post\",\"featured-post\",\"upload-a-theme\"]",
"last_seen": "2017-07-01T12:30:37.000Z",
"created_at": "2017-09-01T12:29:51.000Z",
"created_by": "1",
"updated_at": "2017-09-01T12:30:59.000Z",
"updated_by": "1"
}
],
"posts_authors": [
@ -291,11 +319,23 @@
"post_id": "59a952be7d79ed06b0d21129",
"author_id": "5951f5fca366002ebd5dbefff",
"sort_order": 1
}, {
},
{
"id": "5a8c0aa22a49c40927b18479",
"post_id": "59a952be7d79ed06b0d21129",
"author_id": "5951f5fca366002ebd5dbef10",
"sort_order": 2
},
{
"id": "5a8c0aa22a49c40927b18479",
"post_id": "59a952be7d79ed06b0d21129",
"author_id": "5951f5fca366002ebd5dbef10",
"sort_order": 2
},
{
"id": "5a8c0aa22a49c40927b18480",
"post_id": "59a952be7d79ed06b0d21128",
"author_id": "5951f5fca366002ebd5dbef11"
}
]
}