diff --git a/core/server/api/v2/actions.js b/core/server/api/v2/actions.js new file mode 100644 index 0000000000..182bdee70f --- /dev/null +++ b/core/server/api/v2/actions.js @@ -0,0 +1,38 @@ +const models = require('../../models'); + +module.exports = { + docName: 'actions', + + browse: { + options: [ + 'page', + 'limit', + 'fields' + ], + data: [ + 'id', + 'type' + ], + validation: { + id: { + required: true + }, + type: { + required: true, + values: ['resource', 'actor'] + } + }, + permissions: true, + query(frame) { + if (frame.data.type === 'resource') { + frame.options.withRelated = ['actor']; + frame.options.filter = `resource_id:${frame.data.id}`; + } else { + frame.options.withRelated = ['resource']; + frame.options.filter = `actor_id:${frame.data.id}`; + } + + return models.Action.findPage(frame.options); + } + } +}; diff --git a/core/server/api/v2/index.js b/core/server/api/v2/index.js index ca78354df3..4ca0ee75dc 100644 --- a/core/server/api/v2/index.js +++ b/core/server/api/v2/index.js @@ -109,5 +109,9 @@ module.exports = { get themes() { return shared.pipeline(require('./themes'), localUtils); + }, + + get actions() { + return shared.pipeline(require('./actions'), localUtils); } }; diff --git a/core/server/api/v2/utils/serializers/output/actions.js b/core/server/api/v2/utils/serializers/output/actions.js new file mode 100644 index 0000000000..8e81ed0d70 --- /dev/null +++ b/core/server/api/v2/utils/serializers/output/actions.js @@ -0,0 +1,15 @@ +const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:actions'); +const mapper = require('./utils/mapper'); + +module.exports = { + browse(models, apiConfig, frame) { + debug('browse'); + + frame.response = { + actions: models.data.map(model => mapper.mapAction(model, frame)), + meta: models.meta + }; + + debug(frame.response); + } +}; diff --git a/core/server/api/v2/utils/serializers/output/index.js b/core/server/api/v2/utils/serializers/output/index.js index 56606591c5..e8c4e0a5d6 100644 --- a/core/server/api/v2/utils/serializers/output/index.js +++ b/core/server/api/v2/utils/serializers/output/index.js @@ -89,5 +89,9 @@ module.exports = { get themes() { return require('./themes'); + }, + + get actions() { + return require('./actions'); } }; diff --git a/core/server/api/v2/utils/serializers/output/utils/clean.js b/core/server/api/v2/utils/serializers/output/utils/clean.js index 5b61bf6dd8..f4041f8d66 100644 --- a/core/server/api/v2/utils/serializers/output/utils/clean.js +++ b/core/server/api/v2/utils/serializers/output/utils/clean.js @@ -1,3 +1,4 @@ +const _ = require('lodash'); const localUtils = require('../../../index'); const tag = (attrs) => { @@ -98,6 +99,32 @@ const post = (attrs, frame) => { return attrs; }; +const action = (attrs) => { + if (attrs.actor) { + delete attrs.actor_id; + delete attrs.resource_id; + + if (attrs.actor_type === 'user') { + attrs.actor = _.pick(attrs.actor, ['id', 'name', 'slug', 'profile_image']); + attrs.actor.image = attrs.actor.profile_image; + delete attrs.actor.profile_image; + } else { + attrs.actor = _.pick(attrs.actor, ['id', 'name', 'slug', 'icon_image']); + attrs.actor.image = attrs.actor.icon_image; + delete attrs.actor.icon_image; + } + } else if (attrs.resource) { + delete attrs.actor_id; + delete attrs.resource_id; + + // @NOTE: we only support posts right now + attrs.resource = _.pick(attrs.resource, ['id', 'title', 'slug', 'feature_image']); + attrs.resource.image = attrs.resource.feature_image; + delete attrs.resource.feature_image; + } +}; + module.exports.post = post; module.exports.tag = tag; module.exports.author = author; +module.exports.action = action; diff --git a/core/server/api/v2/utils/serializers/output/utils/mapper.js b/core/server/api/v2/utils/serializers/output/utils/mapper.js index 588349cb6f..995c569528 100644 --- a/core/server/api/v2/utils/serializers/output/utils/mapper.js +++ b/core/server/api/v2/utils/serializers/output/utils/mapper.js @@ -95,9 +95,16 @@ const mapImage = (path) => { return url.forImage(path); }; +const mapAction = (model, frame) => { + const attrs = model.toJSON(frame.options); + clean.action(attrs); + return attrs; +}; + module.exports.mapPost = mapPost; module.exports.mapUser = mapUser; module.exports.mapTag = mapTag; module.exports.mapIntegration = mapIntegration; module.exports.mapSettings = mapSettings; module.exports.mapImage = mapImage; +module.exports.mapAction = mapAction; diff --git a/core/server/services/permissions/parse-context.js b/core/server/services/permissions/parse-context.js index 951d6ecbeb..14315c0851 100644 --- a/core/server/services/permissions/parse-context.js +++ b/core/server/services/permissions/parse-context.js @@ -13,6 +13,7 @@ module.exports = function parseContext(context) { user: null, api_key: null, app: null, + integration: null, public: true }; @@ -34,6 +35,7 @@ module.exports = function parseContext(context) { if (context && context.api_key) { parsed.api_key = context.api_key; + parsed.integration = context.integration; parsed.public = (context.api_key.type === 'content'); } diff --git a/core/server/web/api/v2/admin/routes.js b/core/server/web/api/v2/admin/routes.js index 3393b86125..2dc6198564 100644 --- a/core/server/web/api/v2/admin/routes.js +++ b/core/server/web/api/v2/admin/routes.js @@ -256,5 +256,8 @@ module.exports = function apiRoutes() { // ## Oembed (fetch response from oembed provider) router.get('/oembed', mw.authAdminApi, apiv2.http(apiv2.oembed.read)); + // ## Actions + router.get('/actions/:type/:id', mw.authAdminApi, apiv2.http(apiv2.actions.browse)); + return router; }; diff --git a/core/test/acceptance/old/admin/actions_spec.js b/core/test/acceptance/old/admin/actions_spec.js new file mode 100644 index 0000000000..211ba21355 --- /dev/null +++ b/core/test/acceptance/old/admin/actions_spec.js @@ -0,0 +1,142 @@ +const should = require('should'); +const Promise = require('bluebird'); +const supertest = require('supertest'); +const testUtils = require('../../../utils'); +const localUtils = require('./utils'); +const config = require('../../../../server/config'); +const ghost = testUtils.startGhost; + +let request; + +describe('Actions API', function () { + before(function () { + return ghost() + .then(function () { + request = supertest.agent(config.get('url')); + }) + .then(function () { + return localUtils.doAuth(request, 'integrations', 'api_keys'); + }); + }); + + // @NOTE: This test runs a little slower, because we store Dates without milliseconds. + it('Can request actions for resource', function () { + let postId; + + return request + .post(localUtils.API.getApiQuery('posts/')) + .set('Origin', config.get('url')) + .send({ + posts: [{ + title: 'test post' + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201) + .then((res) => { + postId = res.body.posts[0].id; + + return request + .get(localUtils.API.getApiQuery(`actions/resource/${postId}/`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + }) + .then((res) => { + localUtils.API.checkResponse(res.body, 'actions'); + localUtils.API.checkResponse(res.body.actions[0], 'action'); + + res.body.actions.length.should.eql(1); + + res.body.actions[0].resource_type.should.eql('post'); + res.body.actions[0].actor_type.should.eql('user'); + res.body.actions[0].event.should.eql('added'); + Object.keys(res.body.actions[0].actor).length.should.eql(4); + res.body.actions[0].actor.id.should.eql(testUtils.DataGenerator.Content.users[0].id); + res.body.actions[0].actor.image.should.eql(testUtils.DataGenerator.Content.users[0].profile_image); + res.body.actions[0].actor.name.should.eql(testUtils.DataGenerator.Content.users[0].name); + res.body.actions[0].actor.slug.should.eql(testUtils.DataGenerator.Content.users[0].slug); + + return Promise.delay(1000); + }) + .then(() => { + return request + .put(localUtils.API.getApiQuery(`posts/${postId}/`)) + .set('Origin', config.get('url')) + .send({ + posts: [{ + slug: 'new-slug' + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + }) + .then(() => { + return request + .get(localUtils.API.getApiQuery(`actions/resource/${postId}/`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + }) + .then((res) => { + localUtils.API.checkResponse(res.body, 'actions'); + localUtils.API.checkResponse(res.body.actions[0], 'action'); + + res.body.actions.length.should.eql(2); + + res.body.actions[0].resource_type.should.eql('post'); + res.body.actions[0].actor_type.should.eql('user'); + res.body.actions[0].event.should.eql('edited'); + Object.keys(res.body.actions[0].actor).length.should.eql(4); + res.body.actions[0].actor.id.should.eql(testUtils.DataGenerator.Content.users[0].id); + res.body.actions[0].actor.image.should.eql(testUtils.DataGenerator.Content.users[0].profile_image); + res.body.actions[0].actor.name.should.eql(testUtils.DataGenerator.Content.users[0].name); + res.body.actions[0].actor.slug.should.eql(testUtils.DataGenerator.Content.users[0].slug); + + return Promise.delay(1000); + }) + .then(() => { + const integrationRequest = supertest.agent(config.get('url')); + + return integrationRequest + .put(localUtils.API.getApiQuery(`posts/${postId}/`)) + .set('Origin', config.get('url')) + .set('Authorization', `Ghost ${localUtils.getValidAdminToken(localUtils.API.getApiQuery(`posts/${postId}/`))}`) + .send({ + posts: [{ + featured: true + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + }) + .then(() => { + return request + .get(localUtils.API.getApiQuery(`actions/resource/${postId}/`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + }) + .then((res) => { + localUtils.API.checkResponse(res.body, 'actions'); + localUtils.API.checkResponse(res.body.actions[0], 'action'); + + res.body.actions.length.should.eql(3); + + res.body.actions[0].resource_type.should.eql('post'); + res.body.actions[0].actor_type.should.eql('integration'); + res.body.actions[0].event.should.eql('edited'); + Object.keys(res.body.actions[0].actor).length.should.eql(4); + res.body.actions[0].actor.id.should.eql(testUtils.DataGenerator.Content.integrations[0].id); + should.equal(res.body.actions[0].actor.image, null); + res.body.actions[0].actor.name.should.eql(testUtils.DataGenerator.Content.integrations[0].name); + res.body.actions[0].actor.slug.should.eql(testUtils.DataGenerator.Content.integrations[0].slug); + }); + }); +}); diff --git a/core/test/acceptance/old/admin/utils.js b/core/test/acceptance/old/admin/utils.js index 7a5e392423..318cb5c1d2 100644 --- a/core/test/acceptance/old/admin/utils.js +++ b/core/test/acceptance/old/admin/utils.js @@ -17,6 +17,9 @@ const expectedProperties = { slug: ['slug'], invites: ['invites', 'meta'], themes: ['themes'], + actions: ['actions', 'meta'], + + action: ['id', 'resource_type', 'actor_type', 'event', 'created_at', 'actor'], post: _(schema.posts) .keys()