From 1ee4d53bfefd0cbc2c57fe95ba597b62e235bc76 Mon Sep 17 00:00:00 2001 From: Katharina Irrgang Date: Fri, 12 Oct 2018 23:10:43 +0200 Subject: [PATCH] Added tags ctrl to v2 (#10000) refs #9866 --- core/server/api/v2/index.js | 4 + core/server/api/v2/tags.js | 147 ++++++++++++++++ .../api/v2/utils/serializers/output/index.js | 4 + .../api/v2/utils/serializers/output/tags.js | 37 ++++ core/server/web/api/v2/admin/routes.js | 12 +- .../test/functional/api/v2/admin/tags_spec.js | 160 ++++++++++++++++++ 6 files changed, 358 insertions(+), 6 deletions(-) create mode 100644 core/server/api/v2/tags.js create mode 100644 core/server/api/v2/utils/serializers/output/tags.js create mode 100644 core/test/functional/api/v2/admin/tags_spec.js diff --git a/core/server/api/v2/index.js b/core/server/api/v2/index.js index 920439c06b..d09a0d6df1 100644 --- a/core/server/api/v2/index.js +++ b/core/server/api/v2/index.js @@ -53,5 +53,9 @@ module.exports = { get upload() { return shared.pipeline(require('./upload'), localUtils); + }, + + get tags() { + return shared.pipeline(require('./tags'), localUtils); } }; diff --git a/core/server/api/v2/tags.js b/core/server/api/v2/tags.js new file mode 100644 index 0000000000..489c594860 --- /dev/null +++ b/core/server/api/v2/tags.js @@ -0,0 +1,147 @@ +const Promise = require('bluebird'); +const common = require('../../lib/common'); +const models = require('../../models'); +const ALLOWED_INCLUDES = ['count.posts']; + +module.exports = { + docName: 'tags', + + browse: { + options: [ + 'include', + 'filter', + 'fields', + 'limit', + 'order', + 'debug' + ], + validation: { + options: { + include: { + values: ALLOWED_INCLUDES + } + } + }, + permissions: true, + query(frame) { + return models.Tag.findPage(frame.options); + } + }, + + read: { + options: [ + 'include', + 'filter', + 'fields', + 'debug' + ], + data: [ + 'id', + 'slug', + 'visibility' + ], + validation: { + options: { + include: { + values: ALLOWED_INCLUDES + } + } + }, + permissions: true, + query(frame) { + return models.Tag.findOne(frame.data, frame.options) + .then((model) => { + if (!model) { + return Promise.reject(new common.errors.NotFoundError({ + message: common.i18n.t('errors.api.tags.tagNotFound') + })); + } + + return model; + }); + } + }, + + add: { + statusCode: 201, + headers: { + cacheInvalidate: true + }, + options: [ + 'include' + ], + validation: { + options: { + include: { + values: ALLOWED_INCLUDES + } + }, + data: { + name: { + required: true + } + } + }, + permissions: true, + query(frame) { + return models.Tag.add(frame.data.tags[0], frame.options); + } + }, + + edit: { + headers: { + cacheInvalidate: true + }, + options: [ + 'id', + 'include' + ], + validation: { + options: { + include: { + values: ALLOWED_INCLUDES + }, + id: { + required: true + } + } + }, + permissions: true, + query(frame) { + return models.Tag.edit(frame.data.tags[0], frame.options) + .then((model) => { + if (!model) { + return Promise.reject(new common.errors.NotFoundError({ + message: common.i18n.t('errors.api.tags.tagNotFound') + })); + } + + return model; + }); + } + }, + + destroy: { + statusCode: 204, + headers: { + cacheInvalidate: true + }, + options: [ + 'id' + ], + validation: { + options: { + include: { + values: ALLOWED_INCLUDES + }, + id: { + required: true + } + } + }, + permissions: true, + query(frame) { + return models.Tag.destroy(frame.options).return(null); + } + } +}; diff --git a/core/server/api/v2/utils/serializers/output/index.js b/core/server/api/v2/utils/serializers/output/index.js index 99cac5ec85..a5ef1be32d 100644 --- a/core/server/api/v2/utils/serializers/output/index.js +++ b/core/server/api/v2/utils/serializers/output/index.js @@ -41,5 +41,9 @@ module.exports = { get upload() { return require('./upload'); + }, + + get tags() { + return require('./tags'); } }; diff --git a/core/server/api/v2/utils/serializers/output/tags.js b/core/server/api/v2/utils/serializers/output/tags.js new file mode 100644 index 0000000000..469bcc9430 --- /dev/null +++ b/core/server/api/v2/utils/serializers/output/tags.js @@ -0,0 +1,37 @@ +const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:tags'); +const urlService = require('../../../../../services/url'); + +const absoluteUrls = (tag) => { + tag.url = urlService.getUrlByResourceId(tag.id, {absolute: true}); + + if (tag.feature_image) { + tag.feature_image = urlService.utils.urlFor('image', {image: tag.feature_image}, true); + } + + return tag; +}; + +module.exports = { + all(models, apiConfig, frame) { + debug('all'); + + if (!models) { + return; + } + + if (models.meta) { + frame.response = { + tags: models.data.map(model => absoluteUrls(model.toJSON(frame.options))), + meta: models.meta + }; + + return; + } + + frame.response = { + tags: [absoluteUrls(models.toJSON(frame.options))] + }; + + debug(frame.response); + } +}; diff --git a/core/server/web/api/v2/admin/routes.js b/core/server/web/api/v2/admin/routes.js index cf3cf27f2a..e231b2b292 100644 --- a/core/server/web/api/v2/admin/routes.js +++ b/core/server/web/api/v2/admin/routes.js @@ -65,12 +65,12 @@ module.exports = function apiRoutes() { router.del('/users/:id', mw.authAdminAPI, api.http(api.users.destroy)); // ## Tags - router.get('/tags', mw.authAdminAPI, api.http(api.tags.browse)); - router.get('/tags/:id', mw.authAdminAPI, api.http(api.tags.read)); - router.get('/tags/slug/:slug', mw.authAdminAPI, api.http(api.tags.read)); - router.post('/tags', mw.authAdminAPI, api.http(api.tags.add)); - router.put('/tags/:id', mw.authAdminAPI, api.http(api.tags.edit)); - router.del('/tags/:id', mw.authAdminAPI, api.http(api.tags.destroy)); + router.get('/tags', mw.authAdminAPI, apiv2.http(apiv2.tags.browse)); + router.get('/tags/:id', mw.authAdminAPI, apiv2.http(apiv2.tags.read)); + router.get('/tags/slug/:slug', mw.authAdminAPI, apiv2.http(apiv2.tags.read)); + router.post('/tags', mw.authAdminAPI, apiv2.http(apiv2.tags.add)); + router.put('/tags/:id', mw.authAdminAPI, apiv2.http(apiv2.tags.edit)); + router.del('/tags/:id', mw.authAdminAPI, apiv2.http(apiv2.tags.destroy)); // ## Subscribers router.get('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.browse)); diff --git a/core/test/functional/api/v2/admin/tags_spec.js b/core/test/functional/api/v2/admin/tags_spec.js new file mode 100644 index 0000000000..f5aa0b2157 --- /dev/null +++ b/core/test/functional/api/v2/admin/tags_spec.js @@ -0,0 +1,160 @@ +const should = require('should'); +const supertest = require('supertest'); +const testUtils = require('../../../../utils'); +const localUtils = require('./utils'); +const config = require('../../../../../../core/server/config'); +const ghost = testUtils.startGhost; +let request; + +describe('Tag API V2', function () { + let ghostServer; + + before(function () { + return ghost() + .then(function (_ghostServer) { + ghostServer = _ghostServer; + request = supertest.agent(config.get('url')); + }) + .then(function () { + return localUtils.doAuth(request, 'posts'); + }); + }); + + it('browse', function () { + return request + .get(localUtils.API.getApiQuery('tags/?include=count.posts&order=name%20DESC')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.tags); + jsonResponse.tags.should.have.length(6); + testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['count', 'url']); + + testUtils.API.isISO8601(jsonResponse.tags[0].created_at).should.be.true(); + jsonResponse.tags[0].created_at.should.be.an.instanceof(String); + + jsonResponse.meta.pagination.should.have.property('page', 1); + jsonResponse.meta.pagination.should.have.property('limit', 15); + jsonResponse.meta.pagination.should.have.property('pages', 1); + jsonResponse.meta.pagination.should.have.property('total', 6); + jsonResponse.meta.pagination.should.have.property('next', null); + jsonResponse.meta.pagination.should.have.property('prev', null); + + jsonResponse.tags[0].url.should.eql(`${config.get('url')}/tag/pollo/`); + + should.exist(jsonResponse.tags[0].count.posts); + }); + }); + + it('read', function () { + return request + .get(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}/?include=count.posts`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.tags); + jsonResponse.tags.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['count', 'url']); + should.exist(jsonResponse.tags[0].count.posts); + + jsonResponse.tags[0].url.should.eql(`${config.get('url')}/tag/getting-started/`); + }); + }); + + it('add', function () { + const tag = testUtils.DataGenerator.forKnex.createTag(); + + return request + .post(localUtils.API.getApiQuery('tags/')) + .set('Origin', config.get('url')) + .send({ + tags: [tag] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201) + .then((res) => { + should.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.tags); + jsonResponse.tags.should.have.length(1); + // @TODO: model layer has no defaults for these properties + testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['url'], [ + 'feature_image', + 'meta_description', + 'meta_title', + 'parent' + ]); + testUtils.API.isISO8601(jsonResponse.tags[0].created_at).should.be.true(); + }); + }); + + it('add internal', function () { + const tag = testUtils.DataGenerator.forKnex.createTag({ + name: '#test', + slug: null + }); + + return request + .post(localUtils.API.getApiQuery('tags/')) + .set('Origin', config.get('url')) + .send({ + tags: [tag] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201) + .then((res) => { + should.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + jsonResponse.tags[0].visibility.should.eql('internal'); + jsonResponse.tags[0].name.should.eql('#test'); + jsonResponse.tags[0].slug.should.eql('hash-test'); + }); + }); + + it('edit', function () { + return request + .put(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}`)) + .set('Origin', config.get('url')) + .send({ + tags: [Object.assign({}, testUtils.existingData.tags[0], {description: 'hey ho ab ins klo'})] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + should.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.tags); + jsonResponse.tags.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['url']); + jsonResponse.tags[0].description.should.eql('hey ho ab ins klo'); + }); + }); + + it('destroy', function () { + return request + .del(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}`)) + .set('Origin', config.get('url')) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(204) + .then((res) => { + should.exist(res.headers['x-cache-invalidate']); + res.body.should.eql({}); + }); + }); +});