Removed id restriction for posts relations in Admin API v2 (#10489)

refs #10438

- we now try to match by slug or id or email
- fallback to owner
- you cannot create a user via post endpoint
- Ghost uses the invite flow to add users
- get rid of `id` restriction on API level
This commit is contained in:
Katharina Irrgang 2019-02-13 20:38:25 +01:00 committed by GitHub
parent 6bdeeaba10
commit c2b3520652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 36 deletions

View File

@ -134,9 +134,21 @@
"id": { "id": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"slug": {
"type": "string",
"maxLength": 191
},
"email": {
"type": "string",
"maxLength": 191
} }
}, },
"required": ["id"] "anyOf": [
{"required": ["id"]},
{"required": ["slug"]},
{"required": ["email"]}
]
} }
}, },
"post-tags": { "post-tags": {

View File

@ -1,6 +1,7 @@
const _ = require('lodash'), const _ = require('lodash');
Promise = require('bluebird'), const Promise = require('bluebird');
common = require('../../lib/common'); const common = require('../../lib/common');
const sequence = require('../../lib/promise/sequence');
/** /**
* Why and when do we have to fetch `authors` by default? * Why and when do we have to fetch `authors` by default?
@ -100,10 +101,13 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
return this._handleOptions('onUpdating')(model, attrs, options); return this._handleOptions('onUpdating')(model, attrs, options);
}, },
// NOTE: `post.author` was always ignored [unsupported] // @NOTE: `post.author` was always ignored [unsupported]
// @NOTE: triggered before creating and before updating
onSaving: function (model, attrs, options) { onSaving: function (model, attrs, options) {
const ops = [];
/** /**
* @deprecated: `author`, will be removed in Ghost 3.0 * @deprecated: `author`, will be removed in Ghost 3.0, drop v0.1
*/ */
model.unset('author'); model.unset('author');
@ -114,11 +118,47 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
}); });
} }
// CASE: `post.author_id` has changed /**
if (model.hasChanged('author_id')) { * @NOTE:
// CASE: you don't send `post.authors` *
// SOLUTION: we have to update the primary author * Try to find a user with either id, slug or email if "authors" is present.
if (!model.get('authors')) { * Otherwise fallback to owner user.
*
* You cannot create an author via posts!
* Ghost uses the invite flow to create users.
*/
if (model.get('authors')) {
ops.push(() => {
return this.matchAuthors(model, options);
});
}
ops.push(() => {
// CASE: `post.author_id` has changed
if (model.hasChanged('author_id')) {
// CASE: you don't send `post.authors`
// SOLUTION: we have to update the primary author
if (!model.get('authors')) {
let existingAuthors = model.related('authors').toJSON();
// CASE: override primary author
existingAuthors[0] = {
id: model.get('author_id')
};
model.set('authors', existingAuthors);
} else {
// CASE: you send `post.authors` next to `post.author_id`
if (model.get('authors')[0].id !== model.get('author_id')) {
model.set('author_id', model.get('authors')[0].id);
}
}
}
// 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
if (model.hasChanged('author_id') && !model.get('authors')) {
let existingAuthors = model.related('authors').toJSON(); let existingAuthors = model.related('authors').toJSON();
// CASE: override primary author // CASE: override primary author
@ -127,32 +167,15 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
}; };
model.set('authors', existingAuthors); model.set('authors', existingAuthors);
} else { } else if (model.get('authors') && model.get('authors').length) {
// CASE: you send `post.authors` next to `post.author_id` // ensure we update the primary author id
if (model.get('authors')[0].id !== model.get('author_id')) { model.set('author_id', model.get('authors')[0].id);
model.set('author_id', model.get('authors')[0].id);
}
} }
}
// CASE: if you change `post.author_id`, we have to update the primary author return proto.onSaving.call(this, model, attrs, options);
// CASE: if the `author_id` has change and you pass `posts.authors`, we already check above that });
// the primary author id must be equal
if (model.hasChanged('author_id') && !model.get('authors')) {
let existingAuthors = model.related('authors').toJSON();
// CASE: override primary author return sequence(ops);
existingAuthors[0] = {
id: model.get('author_id')
};
model.set('authors', existingAuthors);
} else if (model.get('authors') && model.get('authors').length) {
// ensure we update the primary author id
model.set('author_id', model.get('authors')[0].id);
}
return proto.onSaving.call(this, model, attrs, options);
}, },
serialize: function serialize(options) { serialize: function serialize(options) {
@ -203,6 +226,54 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
} }
return attrs; return attrs;
},
matchAuthors(model, options) {
let ownerUser;
const ops = [];
ops.push(() => {
return ghostBookshelf
.model('User')
.getOwnerUser(Object.assign({columns: ['id']}, _.pick(options, 'transacting')))
.then((_ownerUser) => {
ownerUser = _ownerUser;
});
});
ops.push(() => {
const authors = model.get('authors');
return Promise.each(authors, (author, index) => {
const query = {};
if (author.id) {
query.id = author.id;
} else if (author.slug) {
query.slug = author.slug;
} else if (author.email) {
query.email = author.email;
}
return ghostBookshelf
.model('User')
.where(query)
.fetch(Object.assign({columns: ['id']}, _.pick(options, 'transacting')))
.then((user) => {
authors[index] = {};
if (!user) {
authors[index].id = ownerUser.id;
} else {
authors[index].id = user.id;
}
});
}).then(() => {
model.set('authors', authors);
});
});
return sequence(ops);
} }
}, { }, {
/** /**

View File

@ -20,7 +20,7 @@ describe('Authors Content API', function () {
request = supertest.agent(config.get('url')); request = supertest.agent(config.get('url'));
}) })
.then(function () { .then(function () {
return testUtils.initFixtures('users:no-owner', 'user:inactive', 'posts', 'api_keys'); return testUtils.initFixtures('owner:post', 'users:no-owner', 'user:inactive', 'posts', 'api_keys');
}); });
}); });

View File

@ -19,7 +19,7 @@ describe('Posts Content API', function () {
request = supertest.agent(config.get('url')); request = supertest.agent(config.get('url'));
}) })
.then(function () { .then(function () {
return testUtils.initFixtures('users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'api_keys'); return testUtils.initFixtures('owner:post', 'users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'api_keys');
}); });
}); });