mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
Added Url Service to track all URLs in the system (#9247)
refs #9192 - Introduces a url service that can be initialised - Added a concept of Resources and resource config.json that contains details about the resources in the system that we may want to make customisable - Note that individual resources know how to create their own Urls... this is important for later - Url Service loads all of the resources, and stores their URLs - The UrlService binds to all events, so that when a resource changes its url and related data can be updated if needed - There is a temporary config guard so that this can be turned off easily
This commit is contained in:
parent
1bb9d4ff00
commit
5e5b90ac29
@ -13,7 +13,10 @@
|
||||
],
|
||||
"array-callback-return": "off",
|
||||
"array-element-newline": "off",
|
||||
"arrow-body-style": "error",
|
||||
"arrow-body-style": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"always"
|
||||
|
@ -28,6 +28,7 @@ var debug = require('ghost-ignition').debug('boot:init'),
|
||||
utils = require('./utils'),
|
||||
|
||||
// Services that need initialisation
|
||||
urlService = require('./services/url'),
|
||||
apps = require('./services/apps'),
|
||||
xmlrpc = require('./services/xmlrpc'),
|
||||
slack = require('./services/slack');
|
||||
@ -62,7 +63,10 @@ function init() {
|
||||
// Initialize xmrpc ping
|
||||
xmlrpc.listen(),
|
||||
// Initialize slack ping
|
||||
slack.listen()
|
||||
slack.listen(),
|
||||
// Url Service
|
||||
urlService.init()
|
||||
|
||||
);
|
||||
}).then(function () {
|
||||
debug('Apps, XMLRPC, Slack done');
|
||||
|
52
core/server/services/url/Resource.js
Normal file
52
core/server/services/url/Resource.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash'),
|
||||
api = require('../../api'),
|
||||
utils = require('../../utils'),
|
||||
prefetchDefaults = {
|
||||
context: {
|
||||
internal: true
|
||||
},
|
||||
limit: 'all'
|
||||
};
|
||||
|
||||
class Resource {
|
||||
constructor(config) {
|
||||
this.name = config.name;
|
||||
this.api = config.api;
|
||||
this.prefetchOptions = config.prefetchOptions || {};
|
||||
this.urlLookup = config.urlLookup || config.name;
|
||||
this.events = config.events;
|
||||
this.items = {};
|
||||
}
|
||||
|
||||
fetchAll() {
|
||||
const options = _.defaults(this.prefetchOptions, prefetchDefaults);
|
||||
|
||||
return api[this.api]
|
||||
.browse(options)
|
||||
.then((resp) => {
|
||||
this.items = resp[this.api];
|
||||
return this.items;
|
||||
});
|
||||
}
|
||||
|
||||
toUrl(item) {
|
||||
const data = {
|
||||
[this.urlLookup]: item
|
||||
};
|
||||
return utils.url.urlFor(this.urlLookup, data);
|
||||
}
|
||||
|
||||
toData(item) {
|
||||
return {
|
||||
slug: item.slug,
|
||||
resource: {
|
||||
type: this.name,
|
||||
id: item.id
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Resource;
|
122
core/server/services/url/UrlService.js
Normal file
122
core/server/services/url/UrlService.js
Normal file
@ -0,0 +1,122 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* # URL Service
|
||||
*
|
||||
* This file defines a class of URLService, which serves as a centralised place to handle
|
||||
* generating, storing & fetching URLs of all kinds.
|
||||
*/
|
||||
|
||||
const _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
_debug = require('ghost-ignition').debug._base,
|
||||
debug = _debug('ghost:services:url'),
|
||||
events = require('../../events'),
|
||||
// TODO: make this dynamic
|
||||
resourceConfig = require('./config.json'),
|
||||
Resource = require('./Resource'),
|
||||
urlCache = require('./cache'),
|
||||
utils = require('../../utils');
|
||||
|
||||
class UrlService {
|
||||
constructor() {
|
||||
this.resources = [];
|
||||
|
||||
_.each(resourceConfig, (config) => {
|
||||
this.resources.push(new Resource(config));
|
||||
});
|
||||
}
|
||||
|
||||
bind() {
|
||||
const eventHandlers = {
|
||||
add(model, resource) {
|
||||
UrlService.cacheResourceItem(resource, model.toJSON());
|
||||
},
|
||||
update(model, resource) {
|
||||
const newItem = model.toJSON();
|
||||
const oldItem = model.updatedAttributes();
|
||||
|
||||
const oldUrl = resource.toUrl(oldItem);
|
||||
const storedData = urlCache.get(oldUrl);
|
||||
|
||||
const newUrl = resource.toUrl(newItem);
|
||||
const newData = resource.toData(newItem);
|
||||
|
||||
debug('update', oldUrl, newUrl);
|
||||
if (oldUrl && oldUrl !== newUrl && storedData) {
|
||||
// CASE: we are updating a cached item and the URL has changed
|
||||
debug('Changing URL, unset first');
|
||||
urlCache.unset(oldUrl);
|
||||
}
|
||||
|
||||
// CASE: the URL is either new, or the same, this will create or update
|
||||
urlCache.set(newUrl, newData);
|
||||
},
|
||||
|
||||
remove(model, resource) {
|
||||
const url = resource.toUrl(model.toJSON());
|
||||
urlCache.unset(url);
|
||||
},
|
||||
|
||||
reload(model, resource) {
|
||||
// @TODO: get reload working, so that permalink changes are reflected
|
||||
// NOTE: the current implementation of sitemaps doesn't have this
|
||||
debug('Need to reload all resources: ' + resource.name);
|
||||
}
|
||||
};
|
||||
|
||||
_.each(this.resources, (resource) => {
|
||||
_.each(resource.events, (method, eventName) => {
|
||||
events.on(eventName, (model) => {
|
||||
eventHandlers[method].call(this, model, resource, eventName);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetchAll() {
|
||||
return Promise.each(this.resources, (resource) => {
|
||||
return resource.fetchAll();
|
||||
});
|
||||
}
|
||||
|
||||
loadResourceUrls() {
|
||||
debug('load start');
|
||||
|
||||
this.fetchAll()
|
||||
.then(() => {
|
||||
debug('load end, start processing');
|
||||
|
||||
_.each(this.resources, (resource) => {
|
||||
_.each(resource.items, (item) => {
|
||||
UrlService.cacheResourceItem(resource, item);
|
||||
});
|
||||
});
|
||||
|
||||
debug('processing done, url cache built. Number urls', _.size(urlCache.getAll()));
|
||||
// Wrap this in a check, because else this is a HUGE amount of output
|
||||
// To output this, use DEBUG=ghost:*,ghost-url
|
||||
if (_debug.enabled('ghost-url')) {
|
||||
debug('url-cache', require('util').inspect(urlCache.getAll(), false, null));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
debug('load error', err);
|
||||
});
|
||||
}
|
||||
|
||||
static cacheResourceItem(resource, item) {
|
||||
const url = resource.toUrl(item);
|
||||
const data = resource.toData(item);
|
||||
|
||||
urlCache.set(url, data);
|
||||
}
|
||||
|
||||
static cacheRoute(relativeUrl, data) {
|
||||
const url = utils.url.urlFor({relativeUrl: relativeUrl});
|
||||
data.static = true;
|
||||
urlCache.set(url, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UrlService;
|
39
core/server/services/url/cache.js
Normal file
39
core/server/services/url/cache.js
Normal file
@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
// Based heavily on the settings cache
|
||||
const _ = require('lodash'),
|
||||
debug = require('ghost-ignition').debug('services:url:cache'),
|
||||
events = require('../../events'),
|
||||
urlCache = {};
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Get the entire cache object
|
||||
* Uses clone to prevent modifications from being reflected
|
||||
* @return {{}} urlCache
|
||||
*/
|
||||
getAll() {
|
||||
return _.cloneDeep(urlCache);
|
||||
},
|
||||
set(key, value) {
|
||||
const existing = this.get(key);
|
||||
|
||||
if (!existing) {
|
||||
debug('adding url', key);
|
||||
urlCache[key] = _.cloneDeep(value);
|
||||
events.emit('url.added', key, value);
|
||||
} else if (!_.isEqual(value, existing)) {
|
||||
debug('overwriting url', key);
|
||||
urlCache[key] = _.cloneDeep(value);
|
||||
events.emit('url.edited', key, value);
|
||||
}
|
||||
},
|
||||
unset(key) {
|
||||
const value = this.get(key);
|
||||
delete urlCache[key];
|
||||
debug('removing url', key);
|
||||
events.emit('url.removed', key, value);
|
||||
},
|
||||
get(key) {
|
||||
return _.cloneDeep(urlCache[key]);
|
||||
}
|
||||
};
|
55
core/server/services/url/config.json
Normal file
55
core/server/services/url/config.json
Normal file
@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"name": "post",
|
||||
"api" : "posts",
|
||||
"prefetchOptions": {
|
||||
"filter": "visibility:public+status:published+page:false",
|
||||
"include": "author,tags"
|
||||
},
|
||||
"events": {
|
||||
"post.published": "add",
|
||||
"post.published.edited": "update",
|
||||
"post.unpublished": "remove",
|
||||
"settings.permalinks.edited": "reload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"api" : "posts",
|
||||
"prefetchOptions": {
|
||||
"filter": "visibility:public+status:published+page:true",
|
||||
"include": "author,tags"
|
||||
},
|
||||
"urlLookup": "post",
|
||||
"events": {
|
||||
"page.published": "add",
|
||||
"page.published.edited": "update",
|
||||
"page.unpublished": "remove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tag",
|
||||
"api" : "tags",
|
||||
"prefetchOptions": {
|
||||
"filter": "visibility:public"
|
||||
},
|
||||
"events": {
|
||||
"tag.added": "add",
|
||||
"tag.edited": "update",
|
||||
"tag.deleted": "remove"
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"api" : "users",
|
||||
"prefetchOptions": {
|
||||
"filter": "visibility:public"
|
||||
},
|
||||
"events": {
|
||||
"user.activated": "add",
|
||||
"user.activated.edited": "update",
|
||||
"user.deactivated": "remove"
|
||||
}
|
||||
}
|
||||
]
|
32
core/server/services/url/index.js
Normal file
32
core/server/services/url/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
const debug = require('ghost-ignition').debug('services:url:init'),
|
||||
config = require('../../config'),
|
||||
events = require('../../events'),
|
||||
UrlService = require('./UrlService');
|
||||
|
||||
// @TODO we seriously should move this or make it do almost nothing...
|
||||
module.exports.init = function init() {
|
||||
// Temporary config value just in case this causes problems
|
||||
// @TODO delete this
|
||||
if (config.get('disableUrlService')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Kick off the constructor
|
||||
const urlService = new UrlService();
|
||||
|
||||
urlService.bind();
|
||||
|
||||
// Hardcoded routes
|
||||
// @TODO figure out how to do this from channel or other config
|
||||
// @TODO get rid of name concept (for compat with sitemaps)
|
||||
UrlService.cacheRoute('/', {name: 'home'});
|
||||
// @TODO figure out how to do this from apps
|
||||
// @TODO only do this if subscribe is enabled!
|
||||
UrlService.cacheRoute('/subscribe/', {});
|
||||
|
||||
// Register a listener for server-start to load all the known urls
|
||||
events.on('server:start', function loadAllUrls() {
|
||||
debug('URL service, loading all URLS');
|
||||
urlService.loadResourceUrls();
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user