diff --git a/core/server/models/relations/authors.js b/core/server/models/relations/authors.js index 6469dcaeb3..8cfbb1888e 100644 --- a/core/server/models/relations/authors.js +++ b/core/server/models/relations/authors.js @@ -295,11 +295,13 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) { }, { /** * ### destroyByAuthor - * @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy + * @param {Object} unfilteredOptions has context and id. Context is the user doing the destroy, id is the user to destroy + * @param {string} unfilteredOptions.id + * @param {Object} unfilteredOptions.context + * @param {Object} unfilteredOptions.transacting */ - destroyByAuthor: function destroyByAuthor(unfilteredOptions) { + destroyByAuthor: async function destroyByAuthor(unfilteredOptions) { let options = this.filterOptions(unfilteredOptions, 'destroyByAuthor', {extraAllowedProperties: ['id']}); - let postCollection = Posts.forge(); let authorId = options.id; if (!authorId) { @@ -308,34 +310,84 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) { })); } - // CASE: if you are the primary author of a post, the whole post and it's relations get's deleted. - // `posts_authors` are automatically removed (bookshelf-relations) - // CASE: if you are the secondary author of a post, you are just deleted as author. - // must happen manually - const destroyPost = (() => { - return postCollection - .query('where', 'author_id', '=', authorId) - .fetch(options) - .call('invokeThen', 'destroy', options) - .then(function (response) { - return (options.transacting || ghostBookshelf.knex)('posts_authors') - .where('author_id', authorId) - .del() - .then(() => response); - }) - .catch((err) => { - throw new errors.InternalServerError({err: err}); - }); + const reassignPost = (async () => { + let trx = options.transacting; + let knex = ghostBookshelf.knex; + + try { + // There's only one possible owner per Ghost instance + const ownerUser = await knex('roles') + .transacting(trx) + .join('roles_users', 'roles.id', '=', 'roles_users.role_id') + .where('roles.name', 'Owner') + .select('roles_users.user_id'); + const ownerId = ownerUser[0].user_id; + + const authorsPosts = await knex('posts_authors') + .transacting(trx) + .where('author_id', authorId) + .select('post_id', 'sort_order'); + + const ownersPosts = await knex('posts_authors') + .transacting(trx) + .where('author_id', ownerId) + .select('post_id'); + + const authorsPrimaryPosts = authorsPosts.filter(ap => ap.sort_order === 0); + const primaryPostsWithOwnerCoauthor = _.intersectionBy(authorsPrimaryPosts, ownersPosts, 'post_id'); + const primaryPostsWithOwnerCoauthorIds = primaryPostsWithOwnerCoauthor.map(post => post.post_id); + + // remove author and bump owner's sort_order to 0 to make them a primary author + // remove author from posts + await knex('posts_authors') + .transacting(trx) + .whereIn('post_id', primaryPostsWithOwnerCoauthorIds) + .where('author_id', authorId) + .del(); + + // make the owner a primary author + await knex('posts_authors') + .transacting(trx) + .whereIn('post_id', primaryPostsWithOwnerCoauthorIds) + .where('author_id', ownerId) + .update('sort_order', 0); + + const primaryPostsWithoutOwnerCoauthor = _.differenceBy(authorsPrimaryPosts, primaryPostsWithOwnerCoauthor, 'post_id'); + const postsWithoutOwnerCoauthorIds = primaryPostsWithoutOwnerCoauthor.map(post => post.post_id); + + // swap out current author with the owner + await knex('posts_authors') + .transacting(trx) + .whereIn('post_id', postsWithoutOwnerCoauthorIds) + .where('author_id', authorId) + .update('author_id', ownerId); + + // remove author as secondary author from any other posts + await knex('posts_authors') + .transacting(trx) + .where('author_id', authorId) + .del(); + // --------- secondary author cleanup END + + // make the owner a primary author in post table + // remove this statement once 'author' concept is gone + await knex('posts') + .transacting(trx) + .where('author_id', authorId) + .update('author_id', ownerId); + } catch (err) { + throw new errors.InternalServerError({err: err}); + } }); if (!options.transacting) { return ghostBookshelf.transaction((transacting) => { options.transacting = transacting; - return destroyPost(); + return reassignPost(); }); } - return destroyPost(); + return reassignPost(); }, permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { diff --git a/test/e2e-api/admin/users.test.js b/test/e2e-api/admin/users.test.js index 582e715074..f563fe3bc6 100644 --- a/test/e2e-api/admin/users.test.js +++ b/test/e2e-api/admin/users.test.js @@ -196,8 +196,9 @@ describe('User API', function () { .expect(200); }); - it('Can destroy an active user', async function () { + it('Can destroy an active user and transfer posts to the owner', async function () { const userId = testUtils.getExistingData().users[1].id; + const ownerId = testUtils.getExistingData().users[0].id; const res = await request .get(localUtils.API.getApiQuery(`posts/?filter=author_id:${userId}`)) @@ -206,6 +207,24 @@ describe('User API', function () { res.body.posts.length.should.eql(7); + const ownerPostsAuthorsModels = await db.knex('posts_authors') + .where({ + author_id: ownerId + }) + .select(); + + // includes posts & pages + should.equal(ownerPostsAuthorsModels.length, 8); + + const userPostsAuthorsModels = await db.knex('posts_authors') + .where({ + author_id: userId + }) + .select(); + + // includes posts & pages + should.equal(userPostsAuthorsModels.length, 11); + const res2 = await request .delete(localUtils.API.getApiQuery(`users/${userId}`)) .set('Origin', config.get('url')) @@ -240,6 +259,22 @@ describe('User API', function () { const rolesUsers = await db.knex('roles_users').select(); rolesUsers.length.should.greaterThan(0); + + const ownerPostsAuthorsModelsAfter = await db.knex('posts_authors') + .where({ + author_id: ownerId + }) + .select(); + + should.equal(ownerPostsAuthorsModelsAfter.length, 19); + + const userPostsAuthorsModelsAfter = await db.knex('posts_authors') + .where({ + author_id: userId + }) + .select(); + + should.equal(userPostsAuthorsModelsAfter.length, 0); }); it('Can transfer ownership to admin user', async function () {