0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added Ghost Explore endpoint

- this new endpoint returns a special set of data for use in Ghost Explore
This commit is contained in:
Daniel Lockyer 2022-07-06 09:51:11 +02:00
parent 4134a6ac4c
commit 1dd83e1a0f
11 changed files with 215 additions and 0 deletions

View file

@ -0,0 +1,12 @@
const exploreService = require('../../services/explore');
module.exports = {
docName: 'explore',
read: {
permissions: true,
query() {
return exploreService.fetchData();
}
}
};

View file

@ -137,6 +137,10 @@ module.exports = {
return shared.pipeline(require('./config'), localUtils);
},
get explore() {
return shared.pipeline(require('./explore'), localUtils);
},
get themes() {
return shared.pipeline(require('./themes'), localUtils);
},

View file

@ -0,0 +1,11 @@
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:explore');
module.exports = {
all(data, apiConfig, frame) {
debug('all');
frame.response = {
explore: data
};
}
};

View file

@ -21,6 +21,10 @@ module.exports = {
return require('./db');
},
get explore() {
return require('./explore');
},
get pages() {
return require('./pages');
},

View file

@ -0,0 +1,18 @@
const ExploreService = require('./service');
const MembersService = require('../members');
const PostsService = require('../posts/posts-service')();
const PublicConfigService = require('../public-config');
const StatsService = require('../stats');
const StripeService = require('../stripe');
const models = require('../../models');
module.exports = new ExploreService({
MembersService,
PostsService,
PublicConfigService,
StatsService,
StripeService,
UserModel: models.User
});

View file

@ -0,0 +1,55 @@
const ghostVersion = require('@tryghost/version');
module.exports = class ExploreService {
/**
* @param {Object} options
* @param {Object} options.MembersService
* @param {Object} options.PostsService
* @param {Object} options.PublicConfigService
* @param {Object} options.StatsService
* @param {Object} options.StripeService
* @param {Object} options.UserModel
*/
constructor({MembersService, PostsService, PublicConfigService, StatsService, StripeService, UserModel}) {
this.MembersService = MembersService;
this.PostsService = PostsService;
this.PublicConfigService = PublicConfigService;
this.StatsService = StatsService;
this.StripeService = StripeService;
this.UserModel = UserModel;
}
/**
* Build and return the response object containing the data for the Ghost Explore endpoint
*/
async fetchData() {
const totalMembers = await this.MembersService.stats.getTotalMembers();
const mrrStats = await this.StatsService.getMRRHistory();
const {description, icon, title, url} = this.PublicConfigService.site;
const exploreProperties = {
version: ghostVersion.full,
totalMembers,
mrrStats,
site: {
description,
icon,
title,
url
},
stripe: {
configured: this.StripeService.api.configured,
livemode: (this.StripeService.api.configured && this.StripeService.api.mode === 'live')
}
};
const mostRecentlyPublishedPost = await this.PostsService.getMostRecentlyPublishedPost();
exploreProperties.mostRecentlyPublishedAt = mostRecentlyPublishedPost?.get('published_at') || null;
const owner = await this.UserModel.findOne({role: 'Owner', status: 'all'});
exploreProperties.ownerEmail = owner?.get('email') || null;
return exploreProperties;
}
};

View file

@ -76,6 +76,20 @@ class PostsService {
}
}
/**
* Returns the most recently `published_at` post that was published or sent
* via email
*/
async getMostRecentlyPublishedPost() {
const recentlyPublishedPost = await this.models.Post.findPage({
status: ['published', 'sent'],
order: 'published_at DESC',
limit: 1
});
return recentlyPublishedPost?.data[0];
}
/**
* Calculates if the email should be tried to be sent out
* @private

View file

@ -33,6 +33,7 @@ const notImplemented = function (req, res, next) {
offers: ['GET', 'PUT', 'POST'],
newsletters: ['GET', 'PUT', 'POST'],
config: ['GET'],
explore: ['GET'],
schedules: ['PUT'],
files: ['POST'],
media: ['POST'],

View file

@ -20,6 +20,9 @@ module.exports = function apiRoutes() {
// ## Configuration
router.get('/config', mw.authAdminApi, http(api.config.read));
// ## Ghost Explore
router.get('/explore', mw.authAdminApi, http(api.explore.read));
// ## Posts
router.get('/posts', mw.authAdminApi, http(api.posts.browse));
router.post('/posts', mw.authAdminApi, http(api.posts.add));

View file

@ -0,0 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explore API Read Can request Explore data 1: [body] 1`] = `
Object {
"explore": Object {
"mostRecentlyPublishedAt": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"mrrStats": Object {
"data": Array [
Object {
"currency": "usd",
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"mrr": 0,
},
Object {
"currency": "usd",
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"mrr": 1000,
},
],
"meta": Object {
"totals": Array [
Object {
"currency": "usd",
"mrr": 1000,
},
],
},
},
"ownerEmail": "jbloggs@example.com",
"site": Object {
"description": "Thoughts, stories and ideas",
"icon": null,
"title": "Ghost",
"url": "http://127.0.0.1:2369/",
},
"stripe": Object {
"configured": true,
"livemode": false,
},
"totalMembers": 8,
"version": StringMatching /\\\\d\\+\\\\\\.\\\\d\\+\\\\\\.\\\\d\\+/,
},
}
`;
exports[`Explore API Read Can request Explore data 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "463",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;

View file

@ -0,0 +1,37 @@
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyISODate, anyISODateTime, stringMatching} = matchers;
describe('Explore API', function () {
let agent;
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('members');
await agent.loginAsOwner();
});
describe('Read', function () {
it('Can request Explore data', async function () {
await agent
.get('explore/')
.expectStatus(200)
.matchBodySnapshot({
explore: {
mostRecentlyPublishedAt: anyISODateTime,
mrrStats: {
data: [{
date: anyISODate
},
{
date: anyISODate
}]
},
version: stringMatching(/\d+\.\d+\.\d+/)
}
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
});
});