mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +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-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"
|
||||||
|
@ -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');
|
||||||
|
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