mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 11:55:01 +03:00
Added plugin based author and public tag models in API v2 (#10284)
refs #10124 - Author model returns only users that have published non-page posts - Added a public controller for tags (should be extracted to separate Content API controller https://github.com/TryGhost/Ghost/issues/10106) - Made resource configuration dynamic based on current theme engine - This needs a follow-up PR with fixes to the problems described in the PR
This commit is contained in:
parent
75037bcb28
commit
d3f3b3dc20
@ -25,7 +25,7 @@ module.exports = {
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.User.findPage(frame.options);
|
||||
return models.Author.findPage(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
@ -51,7 +51,7 @@ module.exports = {
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.User.findOne(frame.data, frame.options)
|
||||
return models.Author.findOne(frame.data, frame.options)
|
||||
.then((model) => {
|
||||
if (!model) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
|
@ -67,6 +67,10 @@ module.exports = {
|
||||
return shared.pipeline(require('./tags'), localUtils);
|
||||
},
|
||||
|
||||
get tagsPublic() {
|
||||
return shared.pipeline(require('./tags-public'), localUtils);
|
||||
},
|
||||
|
||||
get users() {
|
||||
return shared.pipeline(require('./users'), localUtils);
|
||||
},
|
||||
|
66
core/server/api/v2/tags-public.js
Normal file
66
core/server/api/v2/tags-public.js
Normal file
@ -0,0 +1,66 @@
|
||||
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',
|
||||
'page',
|
||||
'debug'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: ALLOWED_INCLUDES
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.TagPublic.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.TagPublic.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;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
|
||||
const ALLOWED_INCLUDES = ['count.posts'];
|
||||
|
||||
module.exports = {
|
||||
|
@ -158,6 +158,7 @@ class BaseSiteMapGenerator {
|
||||
reset() {
|
||||
this.nodeLookup = {};
|
||||
this.nodeTimeLookup = {};
|
||||
this.siteMapContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ var proxy = require('./proxy'),
|
||||
|
||||
/**
|
||||
* v0.1: users, posts, tags
|
||||
* v2: authors, pages, posts, tags
|
||||
* v2: authors, pages, posts, tagsPublic
|
||||
*
|
||||
* @NOTE: if you use "users" in v2, we should fallback to authors
|
||||
*/
|
||||
@ -27,7 +27,7 @@ const RESOURCES = {
|
||||
resource: 'posts'
|
||||
},
|
||||
tags: {
|
||||
alias: 'tags',
|
||||
alias: 'tagsPublic',
|
||||
resource: 'tags'
|
||||
},
|
||||
users: {
|
||||
|
18
core/server/models/author.js
Normal file
18
core/server/models/author.js
Normal file
@ -0,0 +1,18 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
const user = require('./user');
|
||||
|
||||
const Author = user.User.extend({
|
||||
shouldHavePosts: {
|
||||
joinTo: 'author_id',
|
||||
joinTable: 'posts_authors'
|
||||
}
|
||||
});
|
||||
|
||||
const Authors = ghostBookshelf.Collection.extend({
|
||||
model: Author
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Author: ghostBookshelf.model('Author', Author),
|
||||
Authors: ghostBookshelf.collection('Authors', Authors)
|
||||
};
|
@ -46,6 +46,9 @@ ghostBookshelf.plugin(plugins.pagination);
|
||||
// Update collision plugin
|
||||
ghostBookshelf.plugin(plugins.collision);
|
||||
|
||||
// Load hasPosts plugin for authors models
|
||||
ghostBookshelf.plugin(plugins.hasPosts);
|
||||
|
||||
// Manages nested updates (relationships)
|
||||
ghostBookshelf.plugin('bookshelf-relations', {
|
||||
allowedOptions: ['context', 'importing', 'migrating'],
|
||||
@ -1030,6 +1033,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
};
|
||||
const exclude = options.exclude;
|
||||
const filter = options.filter;
|
||||
const shouldHavePosts = options.shouldHavePosts;
|
||||
const withRelated = options.withRelated;
|
||||
const withRelatedFields = options.withRelatedFields;
|
||||
const relations = {
|
||||
@ -1085,6 +1089,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
// @NOTE: We can't use the filter plugin, because we are not using bookshelf.
|
||||
nql(filter).querySQL(query);
|
||||
|
||||
if (shouldHavePosts) {
|
||||
require('../plugins/has-posts').addHasPostsWhere(tableNames[modelName], shouldHavePosts)(query);
|
||||
}
|
||||
|
||||
return query.then((objects) => {
|
||||
debug('fetched', modelName, filter);
|
||||
|
||||
|
@ -29,7 +29,9 @@ models = [
|
||||
'session',
|
||||
'subscriber',
|
||||
'tag',
|
||||
'tag-public',
|
||||
'user',
|
||||
'author',
|
||||
'invite',
|
||||
'webhook',
|
||||
'integration',
|
||||
|
60
core/server/models/plugins/has-posts.js
Normal file
60
core/server/models/plugins/has-posts.js
Normal file
@ -0,0 +1,60 @@
|
||||
const _ = require('lodash');
|
||||
const _debug = require('ghost-ignition').debug._base;
|
||||
const debug = _debug('ghost-query');
|
||||
|
||||
const addHasPostsWhere = (tableName, config) => {
|
||||
const comparisonField = `${tableName}.id`;
|
||||
|
||||
return function (qb) {
|
||||
return qb.whereIn(comparisonField, function () {
|
||||
const innerQb = this
|
||||
.distinct(`${config.joinTable}.${config.joinTo}`)
|
||||
.select()
|
||||
.from(config.joinTable)
|
||||
.whereRaw(`${config.joinTable}.${config.joinTo} = ${comparisonField}`)
|
||||
.join('posts', 'posts.id', `${config.joinTable}.post_id`)
|
||||
.andWhere('posts.status', '=', 'published');
|
||||
|
||||
debug(`QUERY has posts: ${innerQb.toSQL().sql}`);
|
||||
|
||||
return innerQb;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const hasPosts = function hasPosts(Bookshelf) {
|
||||
const modelPrototype = Bookshelf.Model.prototype;
|
||||
|
||||
Bookshelf.Model = Bookshelf.Model.extend({
|
||||
initialize: function () {
|
||||
return modelPrototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
if (this.shouldHavePosts) {
|
||||
this.query(addHasPostsWhere(_.result(this, 'tableName'), this.shouldHavePosts));
|
||||
}
|
||||
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
return modelPrototype.fetch.apply(this, arguments);
|
||||
},
|
||||
|
||||
fetchAll: function () {
|
||||
if (this.shouldHavePosts) {
|
||||
this.query(addHasPostsWhere(_.result(this, 'tableName'), this.shouldHavePosts));
|
||||
}
|
||||
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
return modelPrototype.fetchAll.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = hasPosts;
|
||||
module.exports.addHasPostsWhere = addHasPostsWhere;
|
@ -3,5 +3,6 @@ module.exports = {
|
||||
includeCount: require('./include-count'),
|
||||
pagination: require('./pagination'),
|
||||
collision: require('./collision'),
|
||||
transactionEvents: require('./transaction-events')
|
||||
transactionEvents: require('./transaction-events'),
|
||||
hasPosts: require('./has-posts')
|
||||
};
|
||||
|
18
core/server/models/tag-public.js
Normal file
18
core/server/models/tag-public.js
Normal file
@ -0,0 +1,18 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
const tag = require('./tag');
|
||||
|
||||
const TagPublic = tag.Tag.extend({
|
||||
shouldHavePosts: {
|
||||
joinTo: 'tag_id',
|
||||
joinTable: 'posts_tags'
|
||||
}
|
||||
});
|
||||
|
||||
const TagsPublic = ghostBookshelf.Collection.extend({
|
||||
model: TagPublic
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
TagPublic: ghostBookshelf.model('TagPublic', TagPublic),
|
||||
TagsPublic: ghostBookshelf.collection('TagsPublic', TagsPublic)
|
||||
};
|
@ -83,8 +83,22 @@ module.exports = {
|
||||
// no need to check the score, activation should be used in combination with validate.check
|
||||
// Use the two theme objects to set the current active theme
|
||||
try {
|
||||
let previousGhostAPI;
|
||||
|
||||
if (this.getActive()) {
|
||||
previousGhostAPI = this.getActive().engine('ghost-api');
|
||||
}
|
||||
|
||||
active.set(loadedTheme, checkedTheme, error);
|
||||
const currentGhostAPI = this.getActive().engine('ghost-api');
|
||||
|
||||
common.events.emit('services.themes.activated');
|
||||
|
||||
if (previousGhostAPI !== undefined && (previousGhostAPI !== currentGhostAPI)) {
|
||||
common.events.emit('services.themes.api.changed');
|
||||
const siteApp = require('../../web/site/app');
|
||||
siteApp.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
common.logging.error(new common.errors.InternalServerError({
|
||||
message: common.i18n.t('errors.middleware.themehandler.activateFailed', {theme: loadedTheme.name}),
|
||||
|
@ -1,144 +1,21 @@
|
||||
const debug = require('ghost-ignition').debug('services:url:resources'),
|
||||
Promise = require('bluebird'),
|
||||
_ = require('lodash'),
|
||||
Resource = require('./Resource'),
|
||||
config = require('../../config'),
|
||||
models = require('../../models'),
|
||||
common = require('../../lib/common');
|
||||
const _ = require('lodash');
|
||||
const Promise = require('bluebird');
|
||||
const debug = require('ghost-ignition').debug('services:url:resources');
|
||||
const Resource = require('./Resource');
|
||||
const config = require('../../config');
|
||||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
|
||||
/**
|
||||
* These are the default resources and filters.
|
||||
* These are the minimum filters for public accessibility of resources.
|
||||
*/
|
||||
const resourcesConfig = [
|
||||
{
|
||||
type: 'posts',
|
||||
modelOptions: {
|
||||
modelName: 'Post',
|
||||
filter: 'visibility:public+status:published+page:false',
|
||||
exclude: [
|
||||
'title',
|
||||
'mobiledoc',
|
||||
'html',
|
||||
'plaintext',
|
||||
'amp',
|
||||
'codeinjection_head',
|
||||
'codeinjection_foot',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'custom_excerpt',
|
||||
'og_image',
|
||||
'og_title',
|
||||
'og_description',
|
||||
'twitter_image',
|
||||
'twitter_title',
|
||||
'twitter_description',
|
||||
'custom_template',
|
||||
'feature_image',
|
||||
'locale'
|
||||
],
|
||||
withRelated: ['tags', 'authors'],
|
||||
withRelatedPrimary: {
|
||||
primary_tag: 'tags',
|
||||
primary_author: 'authors'
|
||||
},
|
||||
withRelatedFields: {
|
||||
tags: ['tags.id', 'tags.slug'],
|
||||
authors: ['users.id', 'users.slug']
|
||||
}
|
||||
},
|
||||
events: {
|
||||
add: 'post.published',
|
||||
update: 'post.published.edited',
|
||||
remove: 'post.unpublished'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'pages',
|
||||
modelOptions: {
|
||||
modelName: 'Post',
|
||||
exclude: [
|
||||
'title',
|
||||
'mobiledoc',
|
||||
'html',
|
||||
'plaintext',
|
||||
'amp',
|
||||
'codeinjection_head',
|
||||
'codeinjection_foot',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'custom_excerpt',
|
||||
'og_image',
|
||||
'og_title',
|
||||
'og_description',
|
||||
'twitter_image',
|
||||
'twitter_title',
|
||||
'twitter_description',
|
||||
'custom_template',
|
||||
'feature_image',
|
||||
'locale',
|
||||
'tags',
|
||||
'authors',
|
||||
'primary_tag',
|
||||
'primary_author'
|
||||
],
|
||||
filter: 'visibility:public+status:published+page:true'
|
||||
},
|
||||
events: {
|
||||
add: 'page.published',
|
||||
update: 'page.published.edited',
|
||||
remove: 'page.unpublished'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tags',
|
||||
keep: ['id', 'slug', 'updated_at', 'created_at'],
|
||||
modelOptions: {
|
||||
modelName: 'Tag',
|
||||
exclude: [
|
||||
'description',
|
||||
'meta_title',
|
||||
'meta_description'
|
||||
],
|
||||
filter: 'visibility:public'
|
||||
},
|
||||
events: {
|
||||
add: 'tag.added',
|
||||
update: 'tag.edited',
|
||||
remove: 'tag.deleted'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'authors',
|
||||
modelOptions: {
|
||||
modelName: 'User',
|
||||
exclude: [
|
||||
'bio',
|
||||
'website',
|
||||
'location',
|
||||
'facebook',
|
||||
'twitter',
|
||||
'accessibility',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'tour'
|
||||
],
|
||||
filter: 'visibility:public'
|
||||
},
|
||||
events: {
|
||||
add: 'user.activated',
|
||||
update: 'user.activated.edited',
|
||||
remove: 'user.deactivated'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* NOTE: We are querying knex directly, because the Bookshelf ORM overhead is too slow.
|
||||
* At the moment Resource service is directly responsible for data population
|
||||
* for URLs in UrlService. But because it's actually a storage of all possible
|
||||
* resources in the system, could also be used as a cache for Content API in
|
||||
* the future.
|
||||
*/
|
||||
class Resources {
|
||||
constructor(queue) {
|
||||
this.queue = queue;
|
||||
this.resourcesConfig = [];
|
||||
this.data = {};
|
||||
|
||||
this.listeners = [];
|
||||
@ -160,15 +37,27 @@ class Resources {
|
||||
* Currently the url service needs to use the settings cache,
|
||||
* because we need to `settings.permalink`.
|
||||
*/
|
||||
this._listenOn('db.ready', this._onDatabaseReady.bind(this));
|
||||
this._listenOn('db.ready', this.fetchResources.bind(this));
|
||||
}
|
||||
|
||||
_onDatabaseReady() {
|
||||
_initResourceConfig() {
|
||||
if (!_.isEmpty(this.resourcesConfig)) {
|
||||
return this.resourceConfig;
|
||||
}
|
||||
|
||||
this.resourcesAPIVersion = require('../themes').getActive().engine('ghost-api') || 'v0.1';
|
||||
this.resourcesConfig = require(`./configs/${this.resourcesAPIVersion}`);
|
||||
}
|
||||
|
||||
fetchResources() {
|
||||
const ops = [];
|
||||
debug('db ready. settings cache ready.');
|
||||
this._initResourceConfig();
|
||||
|
||||
_.each(resourcesConfig, (resourceConfig) => {
|
||||
_.each(this.resourcesConfig, (resourceConfig) => {
|
||||
this.data[resourceConfig.type] = [];
|
||||
|
||||
// NOTE: We are querying knex directly, because the Bookshelf ORM overhead is too slow.
|
||||
ops.push(this._fetch(resourceConfig));
|
||||
|
||||
this._listenOn(resourceConfig.events.add, (model) => {
|
||||
@ -223,7 +112,7 @@ class Resources {
|
||||
}
|
||||
|
||||
_onResourceAdded(type, model) {
|
||||
const resourceConfig = _.find(resourcesConfig, {type: type});
|
||||
const resourceConfig = _.find(this.resourcesConfig, {type: type});
|
||||
const exclude = resourceConfig.modelOptions.exclude;
|
||||
const withRelatedFields = resourceConfig.modelOptions.withRelatedFields;
|
||||
const obj = _.omit(model.toJSON(), exclude);
|
||||
@ -296,7 +185,7 @@ class Resources {
|
||||
|
||||
this.data[type].every((resource) => {
|
||||
if (resource.data.id === model.id) {
|
||||
const resourceConfig = _.find(resourcesConfig, {type: type});
|
||||
const resourceConfig = _.find(this.resourcesConfig, {type: type});
|
||||
const exclude = resourceConfig.modelOptions.exclude;
|
||||
const withRelatedFields = resourceConfig.modelOptions.withRelatedFields;
|
||||
const obj = _.omit(model.toJSON(), exclude);
|
||||
@ -401,12 +290,13 @@ class Resources {
|
||||
|
||||
this.listeners = [];
|
||||
this.data = {};
|
||||
this.resourcesConfig = null;
|
||||
}
|
||||
|
||||
softReset() {
|
||||
this.data = {};
|
||||
|
||||
_.each(resourcesConfig, (resourceConfig) => {
|
||||
_.each(this.resourcesConfig, (resourceConfig) => {
|
||||
this.data[resourceConfig.type] = [];
|
||||
});
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ class UrlService {
|
||||
this._onRouterAddedListener = this._onRouterAddedType.bind(this);
|
||||
common.events.on('router.created', this._onRouterAddedListener);
|
||||
|
||||
this._onThemeChangedListener = this._onThemeChangedListener.bind(this);
|
||||
common.events.on('services.themes.api.changed', this._onThemeChangedListener);
|
||||
|
||||
/**
|
||||
* The queue will notify us if url generation has started/finished.
|
||||
*/
|
||||
@ -65,6 +68,11 @@ class UrlService {
|
||||
this.urlGenerators.push(urlGenerator);
|
||||
}
|
||||
|
||||
_onThemeChangedListener() {
|
||||
this.reset({keepListeners: true});
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* You have a url and want to know which the url belongs to.
|
||||
*
|
||||
@ -215,7 +223,11 @@ class UrlService {
|
||||
.getValue(options);
|
||||
}
|
||||
|
||||
reset() {
|
||||
init() {
|
||||
this.resources.fetchResources();
|
||||
}
|
||||
|
||||
reset(options = {}) {
|
||||
debug('reset');
|
||||
this.urlGenerators = [];
|
||||
|
||||
@ -223,9 +235,12 @@ class UrlService {
|
||||
this.queue.reset();
|
||||
this.resources.reset();
|
||||
|
||||
this._onQueueStartedListener && this.queue.removeListener('started', this._onQueueStartedListener);
|
||||
this._onQueueEndedListener && this.queue.removeListener('ended', this._onQueueEndedListener);
|
||||
this._onRouterAddedListener && common.events.removeListener('router.created', this._onRouterAddedListener);
|
||||
if (!options.keepListeners) {
|
||||
this._onQueueStartedListener && this.queue.removeListener('started', this._onQueueStartedListener);
|
||||
this._onQueueEndedListener && this.queue.removeListener('ended', this._onQueueEndedListener);
|
||||
this._onRouterAddedListener && common.events.removeListener('router.created', this._onRouterAddedListener);
|
||||
this._onThemeChangedListener && common.events.removeListener('services.themes.api.changed', this._onThemeChangedListener);
|
||||
}
|
||||
}
|
||||
|
||||
resetGenerators(options = {}) {
|
||||
|
123
core/server/services/url/configs/v0.1.js
Normal file
123
core/server/services/url/configs/v0.1.js
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* These are the default resources and filters.
|
||||
* They contain minimum filters for public accessibility of resources.
|
||||
*/
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
type: 'posts',
|
||||
modelOptions: {
|
||||
modelName: 'Post',
|
||||
filter: 'visibility:public+status:published+page:false',
|
||||
exclude: [
|
||||
'title',
|
||||
'mobiledoc',
|
||||
'html',
|
||||
'plaintext',
|
||||
'amp',
|
||||
'codeinjection_head',
|
||||
'codeinjection_foot',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'custom_excerpt',
|
||||
'og_image',
|
||||
'og_title',
|
||||
'og_description',
|
||||
'twitter_image',
|
||||
'twitter_title',
|
||||
'twitter_description',
|
||||
'custom_template',
|
||||
'feature_image',
|
||||
'locale'
|
||||
],
|
||||
withRelated: ['tags', 'authors'],
|
||||
withRelatedPrimary: {
|
||||
primary_tag: 'tags',
|
||||
primary_author: 'authors'
|
||||
},
|
||||
withRelatedFields: {
|
||||
tags: ['tags.id', 'tags.slug'],
|
||||
authors: ['users.id', 'users.slug']
|
||||
}
|
||||
},
|
||||
events: {
|
||||
add: 'post.published',
|
||||
update: 'post.published.edited',
|
||||
remove: 'post.unpublished'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'pages',
|
||||
modelOptions: {
|
||||
modelName: 'Post',
|
||||
exclude: [
|
||||
'title',
|
||||
'mobiledoc',
|
||||
'html',
|
||||
'plaintext',
|
||||
'amp',
|
||||
'codeinjection_head',
|
||||
'codeinjection_foot',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'custom_excerpt',
|
||||
'og_image',
|
||||
'og_title',
|
||||
'og_description',
|
||||
'twitter_image',
|
||||
'twitter_title',
|
||||
'twitter_description',
|
||||
'custom_template',
|
||||
'feature_image',
|
||||
'locale',
|
||||
'tags',
|
||||
'authors',
|
||||
'primary_tag',
|
||||
'primary_author'
|
||||
],
|
||||
filter: 'visibility:public+status:published+page:true'
|
||||
},
|
||||
events: {
|
||||
add: 'page.published',
|
||||
update: 'page.published.edited',
|
||||
remove: 'page.unpublished'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tags',
|
||||
keep: ['id', 'slug', 'updated_at', 'created_at'],
|
||||
modelOptions: {
|
||||
modelName: 'Tag',
|
||||
exclude: ['description', 'meta_title', 'meta_description'],
|
||||
filter: 'visibility:public'
|
||||
},
|
||||
events: {
|
||||
add: 'tag.added',
|
||||
update: 'tag.edited',
|
||||
remove: 'tag.deleted'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'authors',
|
||||
modelOptions: {
|
||||
modelName: 'User',
|
||||
exclude: [
|
||||
'bio',
|
||||
'website',
|
||||
'location',
|
||||
'facebook',
|
||||
'twitter',
|
||||
'accessibility',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'tour'
|
||||
],
|
||||
filter: 'visibility:public'
|
||||
},
|
||||
events: {
|
||||
add: 'user.activated',
|
||||
update: 'user.activated.edited',
|
||||
remove: 'user.deactivated'
|
||||
}
|
||||
}
|
||||
];
|
131
core/server/services/url/configs/v2.js
Normal file
131
core/server/services/url/configs/v2.js
Normal file
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* These are the default resources and filters.
|
||||
* They contain minimum filters for public accessibility of resources.
|
||||
*/
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
type: 'posts',
|
||||
modelOptions: {
|
||||
modelName: 'Post',
|
||||
filter: 'visibility:public+status:published+page:false',
|
||||
exclude: [
|
||||
'title',
|
||||
'mobiledoc',
|
||||
'html',
|
||||
'plaintext',
|
||||
'amp',
|
||||
'codeinjection_head',
|
||||
'codeinjection_foot',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'custom_excerpt',
|
||||
'og_image',
|
||||
'og_title',
|
||||
'og_description',
|
||||
'twitter_image',
|
||||
'twitter_title',
|
||||
'twitter_description',
|
||||
'custom_template',
|
||||
'feature_image',
|
||||
'locale'
|
||||
],
|
||||
withRelated: ['tags', 'authors'],
|
||||
withRelatedPrimary: {
|
||||
primary_tag: 'tags',
|
||||
primary_author: 'authors'
|
||||
},
|
||||
withRelatedFields: {
|
||||
tags: ['tags.id', 'tags.slug'],
|
||||
authors: ['users.id', 'users.slug']
|
||||
}
|
||||
},
|
||||
events: {
|
||||
add: 'post.published',
|
||||
update: 'post.published.edited',
|
||||
remove: 'post.unpublished'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'pages',
|
||||
modelOptions: {
|
||||
modelName: 'Post',
|
||||
exclude: [
|
||||
'title',
|
||||
'mobiledoc',
|
||||
'html',
|
||||
'plaintext',
|
||||
'amp',
|
||||
'codeinjection_head',
|
||||
'codeinjection_foot',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'custom_excerpt',
|
||||
'og_image',
|
||||
'og_title',
|
||||
'og_description',
|
||||
'twitter_image',
|
||||
'twitter_title',
|
||||
'twitter_description',
|
||||
'custom_template',
|
||||
'feature_image',
|
||||
'locale',
|
||||
'tags',
|
||||
'authors',
|
||||
'primary_tag',
|
||||
'primary_author'
|
||||
],
|
||||
filter: 'visibility:public+status:published+page:true'
|
||||
},
|
||||
events: {
|
||||
add: 'page.published',
|
||||
update: 'page.published.edited',
|
||||
remove: 'page.unpublished'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tags',
|
||||
keep: ['id', 'slug', 'updated_at', 'created_at'],
|
||||
modelOptions: {
|
||||
modelName: 'Tag',
|
||||
exclude: ['description', 'meta_title', 'meta_description'],
|
||||
filter: 'visibility:public',
|
||||
shouldHavePosts: {
|
||||
joinTo: 'tag_id',
|
||||
joinTable: 'posts_tags'
|
||||
}
|
||||
},
|
||||
events: {
|
||||
add: 'tag.added',
|
||||
update: 'tag.edited',
|
||||
remove: 'tag.deleted'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'authors',
|
||||
modelOptions: {
|
||||
modelName: 'User',
|
||||
exclude: [
|
||||
'bio',
|
||||
'website',
|
||||
'location',
|
||||
'facebook',
|
||||
'twitter',
|
||||
'accessibility',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'tour'
|
||||
],
|
||||
filter: 'visibility:public',
|
||||
shouldHavePosts: {
|
||||
joinTo: 'author_id',
|
||||
joinTable: 'posts_authors'
|
||||
}
|
||||
},
|
||||
events: {
|
||||
add: 'user.activated',
|
||||
update: 'user.activated.edited',
|
||||
remove: 'user.deactivated'
|
||||
}
|
||||
}
|
||||
];
|
@ -24,9 +24,9 @@ module.exports = function apiRoutes() {
|
||||
router.get('/authors/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.authors.read));
|
||||
|
||||
// ## Tags
|
||||
router.get('/tags', mw.authenticatePublic, apiv2.http(apiv2.tags.browse));
|
||||
router.get('/tags/:id', mw.authenticatePublic, apiv2.http(apiv2.tags.read));
|
||||
router.get('/tags/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.tags.read));
|
||||
router.get('/tags', mw.authenticatePublic, apiv2.http(apiv2.tagsPublic.browse));
|
||||
router.get('/tags/:id', mw.authenticatePublic, apiv2.http(apiv2.tagsPublic.read));
|
||||
router.get('/tags/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.tagsPublic.read));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
@ -4,15 +4,13 @@ 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;
|
||||
let request;
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
|
@ -45,7 +45,7 @@ describe('Authors Content API V2', function () {
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.authors);
|
||||
localUtils.API.checkResponse(jsonResponse, 'authors');
|
||||
jsonResponse.authors.should.have.length(7);
|
||||
jsonResponse.authors.should.have.length(3);
|
||||
|
||||
// We don't expose the email address, status and other attrs.
|
||||
localUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['url'], null, null);
|
||||
@ -55,9 +55,9 @@ describe('Authors Content API V2', function () {
|
||||
should.exist(url.parse(res.body.authors[0].url).host);
|
||||
|
||||
// Public api returns all authors, but no status! Locked/Inactive authors can still have written articles.
|
||||
models.User.findPage(Object.assign({status: 'all'}, testUtils.context.internal))
|
||||
models.Author.findPage(Object.assign({status: 'all'}, testUtils.context.internal))
|
||||
.then((response) => {
|
||||
_.map(response.data, (model) => model.toJSON()).length.should.eql(7);
|
||||
_.map(response.data, (model) => model.toJSON()).length.should.eql(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -139,29 +139,21 @@ describe('Authors Content API V2', function () {
|
||||
var jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse.authors);
|
||||
jsonResponse.authors.should.have.length(7);
|
||||
jsonResponse.authors.should.have.length(3);
|
||||
|
||||
// We don't expose the email address.
|
||||
localUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['count', 'url'], null, null);
|
||||
|
||||
// Each user should have the correct count
|
||||
// Each user should have the correct count and be more than 0
|
||||
_.find(jsonResponse.authors, {slug:'joe-bloggs'}).count.posts.should.eql(4);
|
||||
_.find(jsonResponse.authors, {slug:'contributor'}).count.posts.should.eql(0);
|
||||
_.find(jsonResponse.authors, {slug:'slimer-mcectoplasm'}).count.posts.should.eql(1);
|
||||
_.find(jsonResponse.authors, {slug:'jimothy-bogendath'}).count.posts.should.eql(0);
|
||||
_.find(jsonResponse.authors, {slug: 'smith-wellingsworth'}).count.posts.should.eql(0);
|
||||
_.find(jsonResponse.authors, {slug:'ghost'}).count.posts.should.eql(7);
|
||||
_.find(jsonResponse.authors, {slug:'inactive'}).count.posts.should.eql(0);
|
||||
|
||||
const ids = jsonResponse.authors
|
||||
.filter(author => (author.slug !== 'ghost'))
|
||||
.filter(author => (author.slug !== 'inactive'))
|
||||
.map(user=> user.id);
|
||||
|
||||
ids.should.eql([
|
||||
testUtils.DataGenerator.Content.users[1].id,
|
||||
testUtils.DataGenerator.Content.users[2].id,
|
||||
testUtils.DataGenerator.Content.users[7].id,
|
||||
testUtils.DataGenerator.Content.users[3].id,
|
||||
testUtils.DataGenerator.Content.users[0].id
|
||||
]);
|
||||
@ -185,7 +177,7 @@ describe('Authors Content API V2', function () {
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.authors);
|
||||
localUtils.API.checkResponse(jsonResponse, 'authors');
|
||||
jsonResponse.authors.should.have.length(7);
|
||||
jsonResponse.authors.should.have.length(3);
|
||||
|
||||
// We don't expose the email address.
|
||||
localUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['count', 'url'], null, null);
|
||||
|
@ -4,19 +4,16 @@ const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const configUtils = require('../../../../utils/configUtils');
|
||||
const config = require('../../../../../../core/server/config');
|
||||
const models = require('../../../../../../core/server/models');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const ghost = testUtils.startGhost;
|
||||
let request;
|
||||
|
||||
describe('Tags Content API V2', function () {
|
||||
let ghostServer;
|
||||
let request;
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
@ -45,7 +42,7 @@ describe('Tags Content API V2', function () {
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.tags);
|
||||
localUtils.API.checkResponse(jsonResponse, 'tags');
|
||||
jsonResponse.tags.should.have.length(15);
|
||||
jsonResponse.tags.should.have.length(4);
|
||||
localUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['url']);
|
||||
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
|
||||
@ -71,7 +68,7 @@ describe('Tags Content API V2', function () {
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.tags);
|
||||
localUtils.API.checkResponse(jsonResponse, 'tags');
|
||||
jsonResponse.tags.should.have.length(56);
|
||||
jsonResponse.tags.should.have.length(4);
|
||||
localUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['url']);
|
||||
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
done();
|
||||
@ -79,7 +76,7 @@ describe('Tags Content API V2', function () {
|
||||
});
|
||||
|
||||
it('browse tags without limit=4 fetches 4 tags', function (done) {
|
||||
request.get(localUtils.API.getApiQuery(`tags/?limit=4&key=${validKey}`))
|
||||
request.get(localUtils.API.getApiQuery(`tags/?limit=3&key=${validKey}`))
|
||||
.set('Origin', testUtils.API.getURL())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
@ -93,7 +90,7 @@ describe('Tags Content API V2', function () {
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.tags);
|
||||
localUtils.API.checkResponse(jsonResponse, 'tags');
|
||||
jsonResponse.tags.should.have.length(4);
|
||||
jsonResponse.tags.should.have.length(3);
|
||||
localUtils.API.checkResponse(jsonResponse.tags[0], 'tag', ['url']);
|
||||
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
done();
|
||||
@ -114,15 +111,13 @@ describe('Tags Content API V2', function () {
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse.tags);
|
||||
jsonResponse.tags.should.be.an.Array().with.lengthOf(56);
|
||||
jsonResponse.tags.should.be.an.Array().with.lengthOf(4);
|
||||
|
||||
// Each tag should have the correct count
|
||||
_.find(jsonResponse.tags, {name: 'Getting Started'}).count.posts.should.eql(7);
|
||||
_.find(jsonResponse.tags, {name: 'kitchen sink'}).count.posts.should.eql(2);
|
||||
_.find(jsonResponse.tags, {name: 'bacon'}).count.posts.should.eql(2);
|
||||
_.find(jsonResponse.tags, {name: 'chorizo'}).count.posts.should.eql(1);
|
||||
_.find(jsonResponse.tags, {name: 'pollo'}).count.posts.should.eql(0);
|
||||
_.find(jsonResponse.tags, {name: 'injection'}).count.posts.should.eql(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ const testUtils = require('../../../utils');
|
||||
const configUtils = require('../../../utils/configUtils');
|
||||
const models = require('../../../../server/models');
|
||||
const common = require('../../../../server/lib/common');
|
||||
const themes = require('../../../../server/services/themes');
|
||||
const UrlService = rewire('../../../../server/services/url/UrlService');
|
||||
const sandbox = sinon.sandbox.create();
|
||||
|
||||
@ -14,6 +15,10 @@ describe('Integration: services/url/UrlService', function () {
|
||||
|
||||
before(function () {
|
||||
models.init();
|
||||
|
||||
sandbox.stub(themes, 'getActive').returns({
|
||||
engine: () => 'v0.1'
|
||||
});
|
||||
});
|
||||
|
||||
before(testUtils.teardown);
|
||||
|
@ -4,6 +4,7 @@ const should = require('should'),
|
||||
moment = require('moment'),
|
||||
testUtils = require('../../utils'),
|
||||
configUtils = require('../../utils/configUtils'),
|
||||
themes = require('../../../server/services/themes'),
|
||||
models = require('../../../server/models'),
|
||||
imageLib = require('../../../server/lib/image'),
|
||||
routing = require('../../../server/services/routing'),
|
||||
@ -277,6 +278,9 @@ describe('{{ghost_head}} helper', function () {
|
||||
// @TODO: this is a LOT of mocking :/
|
||||
sandbox.stub(routing.registry, 'getRssUrl').returns('http://localhost:65530/rss/');
|
||||
sandbox.stub(imageLib.imageSize, 'getImageSizeFromUrl').resolves();
|
||||
sandbox.stub(themes, 'getActive').returns({
|
||||
engine: () => 'v0.1'
|
||||
});
|
||||
|
||||
sandbox.stub(settingsCache, 'get');
|
||||
settingsCache.get.withArgs('title').returns('Ghost');
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
const should = require('should');
|
||||
const _ = require('lodash');
|
||||
const sinon = require('sinon');
|
||||
|
@ -54,8 +54,9 @@ describe('Unit: services/url/UrlService', function () {
|
||||
urlService.queue.addListener.args[0][0].should.eql('started');
|
||||
urlService.queue.addListener.args[1][0].should.eql('ended');
|
||||
|
||||
common.events.on.calledOnce.should.be.true();
|
||||
common.events.on.calledTwice.should.be.true();
|
||||
common.events.on.args[0][0].should.eql('router.created');
|
||||
common.events.on.args[1][0].should.eql('services.themes.api.changed');
|
||||
});
|
||||
|
||||
it('fn: _onQueueStarted', function () {
|
||||
|
Loading…
Reference in New Issue
Block a user