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:
Hannah Wolfe 2017-11-16 13:03:24 +00:00 committed by GitHub
parent 1bb9d4ff00
commit 5e5b90ac29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 309 additions and 2 deletions

View File

@ -13,7 +13,10 @@
], ],
"array-callback-return": "off", "array-callback-return": "off",
"array-element-newline": "off", "array-element-newline": "off",
"arrow-body-style": "error", "arrow-body-style": [
"error",
"always"
],
"arrow-parens": [ "arrow-parens": [
"error", "error",
"always" "always"

View File

@ -28,6 +28,7 @@ var debug = require('ghost-ignition').debug('boot:init'),
utils = require('./utils'), utils = require('./utils'),
// Services that need initialisation // Services that need initialisation
urlService = require('./services/url'),
apps = require('./services/apps'), apps = require('./services/apps'),
xmlrpc = require('./services/xmlrpc'), xmlrpc = require('./services/xmlrpc'),
slack = require('./services/slack'); slack = require('./services/slack');
@ -62,7 +63,10 @@ function init() {
// Initialize xmrpc ping // Initialize xmrpc ping
xmlrpc.listen(), xmlrpc.listen(),
// Initialize slack ping // Initialize slack ping
slack.listen() slack.listen(),
// Url Service
urlService.init()
); );
}).then(function () { }).then(function () {
debug('Apps, XMLRPC, Slack done'); debug('Apps, XMLRPC, Slack done');

View 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;

View 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;

View 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]);
}
};

View 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"
}
}
]

View 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();
});
};