diff --git a/core/server/api/v2/authors.js b/core/server/api/v2/authors.js index 4b49318206..93c5b63aab 100644 --- a/core/server/api/v2/authors.js +++ b/core/server/api/v2/authors.js @@ -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({ diff --git a/core/server/api/v2/index.js b/core/server/api/v2/index.js index 5abd2eb8cc..54c870abb3 100644 --- a/core/server/api/v2/index.js +++ b/core/server/api/v2/index.js @@ -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); }, diff --git a/core/server/api/v2/tags-public.js b/core/server/api/v2/tags-public.js new file mode 100644 index 0000000000..570fbda955 --- /dev/null +++ b/core/server/api/v2/tags-public.js @@ -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; + }); + } + } +}; diff --git a/core/server/api/v2/tags.js b/core/server/api/v2/tags.js index ae019b5af5..1317cc6884 100644 --- a/core/server/api/v2/tags.js +++ b/core/server/api/v2/tags.js @@ -1,6 +1,7 @@ const Promise = require('bluebird'); const common = require('../../lib/common'); const models = require('../../models'); + const ALLOWED_INCLUDES = ['count.posts']; module.exports = { diff --git a/core/server/data/xml/sitemap/base-generator.js b/core/server/data/xml/sitemap/base-generator.js index f44af7753d..9ae755a761 100644 --- a/core/server/data/xml/sitemap/base-generator.js +++ b/core/server/data/xml/sitemap/base-generator.js @@ -158,6 +158,7 @@ class BaseSiteMapGenerator { reset() { this.nodeLookup = {}; this.nodeTimeLookup = {}; + this.siteMapContent = null; } } diff --git a/core/server/helpers/get.js b/core/server/helpers/get.js index 56ac844e86..2ae000f4b9 100644 --- a/core/server/helpers/get.js +++ b/core/server/helpers/get.js @@ -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: { diff --git a/core/server/models/author.js b/core/server/models/author.js new file mode 100644 index 0000000000..8f3ad12d3e --- /dev/null +++ b/core/server/models/author.js @@ -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) +}; diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 0845efff33..99e0ec9c9a 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -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); diff --git a/core/server/models/index.js b/core/server/models/index.js index 366be4742d..5702c4ae5c 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -29,7 +29,9 @@ models = [ 'session', 'subscriber', 'tag', + 'tag-public', 'user', + 'author', 'invite', 'webhook', 'integration', diff --git a/core/server/models/plugins/has-posts.js b/core/server/models/plugins/has-posts.js new file mode 100644 index 0000000000..0f82b46f75 --- /dev/null +++ b/core/server/models/plugins/has-posts.js @@ -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; diff --git a/core/server/models/plugins/index.js b/core/server/models/plugins/index.js index 0fe39fef54..8bed75e95f 100644 --- a/core/server/models/plugins/index.js +++ b/core/server/models/plugins/index.js @@ -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') }; diff --git a/core/server/models/tag-public.js b/core/server/models/tag-public.js new file mode 100644 index 0000000000..084d03a22c --- /dev/null +++ b/core/server/models/tag-public.js @@ -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) +}; diff --git a/core/server/services/themes/index.js b/core/server/services/themes/index.js index 4c9c9b578e..39817995e6 100644 --- a/core/server/services/themes/index.js +++ b/core/server/services/themes/index.js @@ -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}), diff --git a/core/server/services/url/Resources.js b/core/server/services/url/Resources.js index 7961f3b43f..3effa3e4a8 100644 --- a/core/server/services/url/Resources.js +++ b/core/server/services/url/Resources.js @@ -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] = []; }); } diff --git a/core/server/services/url/UrlService.js b/core/server/services/url/UrlService.js index 00061b969c..a9a4df7da7 100644 --- a/core/server/services/url/UrlService.js +++ b/core/server/services/url/UrlService.js @@ -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 = {}) { diff --git a/core/server/services/url/configs/v0.1.js b/core/server/services/url/configs/v0.1.js new file mode 100644 index 0000000000..1c33d8c983 --- /dev/null +++ b/core/server/services/url/configs/v0.1.js @@ -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' + } + } +]; diff --git a/core/server/services/url/configs/v2.js b/core/server/services/url/configs/v2.js new file mode 100644 index 0000000000..a4904ff11f --- /dev/null +++ b/core/server/services/url/configs/v2.js @@ -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' + } + } +]; diff --git a/core/server/web/api/v2/content/routes.js b/core/server/web/api/v2/content/routes.js index 85a1dad9c9..8001b66801 100644 --- a/core/server/web/api/v2/content/routes.js +++ b/core/server/web/api/v2/content/routes.js @@ -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; }; diff --git a/core/test/functional/api/v2/admin/tags_spec.js b/core/test/functional/api/v2/admin/tags_spec.js index d2d8a760d8..4c5dd9135b 100644 --- a/core/test/functional/api/v2/admin/tags_spec.js +++ b/core/test/functional/api/v2/admin/tags_spec.js @@ -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 () { diff --git a/core/test/functional/api/v2/content/authors_spec.js b/core/test/functional/api/v2/content/authors_spec.js index 63ac7c1341..7d66513a4e 100644 --- a/core/test/functional/api/v2/content/authors_spec.js +++ b/core/test/functional/api/v2/content/authors_spec.js @@ -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); diff --git a/core/test/functional/api/v2/content/tags_spec.js b/core/test/functional/api/v2/content/tags_spec.js index 1a81f311db..292c92f1d5 100644 --- a/core/test/functional/api/v2/content/tags_spec.js +++ b/core/test/functional/api/v2/content/tags_spec.js @@ -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(); }); diff --git a/core/test/integration/services/url/UrlService_spec.js b/core/test/integration/services/url/UrlService_spec.js index 5a425ec40d..b93ede6afc 100644 --- a/core/test/integration/services/url/UrlService_spec.js +++ b/core/test/integration/services/url/UrlService_spec.js @@ -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); diff --git a/core/test/unit/helpers/ghost_head_spec.js b/core/test/unit/helpers/ghost_head_spec.js index 1197f135c8..7647ee9d6b 100644 --- a/core/test/unit/helpers/ghost_head_spec.js +++ b/core/test/unit/helpers/ghost_head_spec.js @@ -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'); diff --git a/core/test/unit/services/url/Resources_spec.js b/core/test/unit/services/url/Resources_spec.js index 41c1d6dc6c..3458e153ab 100644 --- a/core/test/unit/services/url/Resources_spec.js +++ b/core/test/unit/services/url/Resources_spec.js @@ -1,4 +1,3 @@ - const should = require('should'); const _ = require('lodash'); const sinon = require('sinon'); diff --git a/core/test/unit/services/url/UrlService_spec.js b/core/test/unit/services/url/UrlService_spec.js index 0ffdc77d3d..928dc7174b 100644 --- a/core/test/unit/services/url/UrlService_spec.js +++ b/core/test/unit/services/url/UrlService_spec.js @@ -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 () {