mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
parent
310526b6c5
commit
1ee4d53bfe
6 changed files with 358 additions and 6 deletions
|
@ -53,5 +53,9 @@ module.exports = {
|
||||||
|
|
||||||
get upload() {
|
get upload() {
|
||||||
return shared.pipeline(require('./upload'), localUtils);
|
return shared.pipeline(require('./upload'), localUtils);
|
||||||
|
},
|
||||||
|
|
||||||
|
get tags() {
|
||||||
|
return shared.pipeline(require('./tags'), localUtils);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
147
core/server/api/v2/tags.js
Normal file
147
core/server/api/v2/tags.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -41,5 +41,9 @@ module.exports = {
|
||||||
|
|
||||||
get upload() {
|
get upload() {
|
||||||
return require('./upload');
|
return require('./upload');
|
||||||
|
},
|
||||||
|
|
||||||
|
get tags() {
|
||||||
|
return require('./tags');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
37
core/server/api/v2/utils/serializers/output/tags.js
Normal file
37
core/server/api/v2/utils/serializers/output/tags.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -65,12 +65,12 @@ module.exports = function apiRoutes() {
|
||||||
router.del('/users/:id', mw.authAdminAPI, api.http(api.users.destroy));
|
router.del('/users/:id', mw.authAdminAPI, api.http(api.users.destroy));
|
||||||
|
|
||||||
// ## Tags
|
// ## Tags
|
||||||
router.get('/tags', mw.authAdminAPI, api.http(api.tags.browse));
|
router.get('/tags', mw.authAdminAPI, apiv2.http(apiv2.tags.browse));
|
||||||
router.get('/tags/:id', mw.authAdminAPI, api.http(api.tags.read));
|
router.get('/tags/:id', mw.authAdminAPI, apiv2.http(apiv2.tags.read));
|
||||||
router.get('/tags/slug/:slug', mw.authAdminAPI, api.http(api.tags.read));
|
router.get('/tags/slug/:slug', mw.authAdminAPI, apiv2.http(apiv2.tags.read));
|
||||||
router.post('/tags', mw.authAdminAPI, api.http(api.tags.add));
|
router.post('/tags', mw.authAdminAPI, apiv2.http(apiv2.tags.add));
|
||||||
router.put('/tags/:id', mw.authAdminAPI, api.http(api.tags.edit));
|
router.put('/tags/:id', mw.authAdminAPI, apiv2.http(apiv2.tags.edit));
|
||||||
router.del('/tags/:id', mw.authAdminAPI, api.http(api.tags.destroy));
|
router.del('/tags/:id', mw.authAdminAPI, apiv2.http(apiv2.tags.destroy));
|
||||||
|
|
||||||
// ## Subscribers
|
// ## Subscribers
|
||||||
router.get('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.browse));
|
router.get('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.browse));
|
||||||
|
|
160
core/test/functional/api/v2/admin/tags_spec.js
Normal file
160
core/test/functional/api/v2/admin/tags_spec.js
Normal file
|
@ -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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue