mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
closes #10773 - The refactoring is a substitute for `urlService.utils` used previously throughout the codebase and now extracted into the separate module in Ghost-SDK - Added url-utils stubbing utility for test suites - Some tests had to be refactored to avoid double mocks (when url's are being reset inside of rested 'describe' groups)
258 lines
7.8 KiB
JavaScript
258 lines
7.8 KiB
JavaScript
const _ = require('lodash'),
|
|
nql = require('@nexes/nql'),
|
|
debug = require('ghost-ignition').debug('services:url:generator'),
|
|
localUtils = require('../../lib/url-utils'),
|
|
// @TODO: merge with filter plugin
|
|
EXPANSIONS = [{
|
|
key: 'author',
|
|
replacement: 'authors.slug'
|
|
}, {
|
|
key: 'tags',
|
|
replacement: 'tags.slug'
|
|
}, {
|
|
key: 'tag',
|
|
replacement: 'tags.slug'
|
|
}, {
|
|
key: 'authors',
|
|
replacement: 'authors.slug'
|
|
}, {
|
|
key: 'primary_tag',
|
|
replacement: 'primary_tag.slug'
|
|
}, {
|
|
key: 'primary_author',
|
|
replacement: 'primary_author.slug'
|
|
}];
|
|
|
|
/**
|
|
* The UrlGenerator class is responsible to generate urls based on a router's conditions.
|
|
* It is the component which sits between routers and resources and connects them together.
|
|
* Each url generator can own resources. Each resource can only be owned by one generator,
|
|
* because each resource can only live on one url at a time.
|
|
*
|
|
* Each router is represented by a url generator.
|
|
*/
|
|
class UrlGenerator {
|
|
constructor(router, queue, resources, urls, position) {
|
|
this.router = router;
|
|
this.queue = queue;
|
|
this.urls = urls;
|
|
this.resources = resources;
|
|
this.uid = position;
|
|
|
|
debug('constructor', this.toString());
|
|
|
|
// CASE: routers can define custom filters, but not required.
|
|
if (this.router.getFilter()) {
|
|
this.filter = this.router.getFilter();
|
|
this.nql = nql(this.filter, {expansions: EXPANSIONS});
|
|
debug('filter', this.filter);
|
|
}
|
|
|
|
this._listeners();
|
|
}
|
|
|
|
/**
|
|
* @description Helper function to register listeners for each url generator instance.
|
|
* @private
|
|
*/
|
|
_listeners() {
|
|
/**
|
|
* @NOTE: currently only used if the permalink setting changes and it's used for this url generator.
|
|
* @TODO: https://github.com/TryGhost/Ghost/issues/10699
|
|
*/
|
|
this.router.addListener('updated', () => {
|
|
const myResources = this.urls.getByGeneratorId(this.uid);
|
|
|
|
myResources.forEach((object) => {
|
|
this.urls.removeResourceId(object.resource.data.id);
|
|
object.resource.release();
|
|
this._try(object.resource);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Listen on two events:
|
|
*
|
|
* - init: bootstrap or url reset
|
|
* - added: resource was added to the database
|
|
*/
|
|
this.queue.register({
|
|
event: 'init',
|
|
tolerance: 100
|
|
}, this._onInit.bind(this));
|
|
|
|
this.queue.register({
|
|
event: 'added'
|
|
}, this._onAdded.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @description Listener which get's called when the resources were fully fetched from the database.
|
|
*
|
|
* Each url generator will be called and can try to own resources now.
|
|
*
|
|
* @private
|
|
*/
|
|
_onInit() {
|
|
debug('_onInit', this.router.getResourceType());
|
|
|
|
// @NOTE: get the resources of my type e.g. posts.
|
|
const resources = this.resources.getAllByType(this.router.getResourceType());
|
|
|
|
debug(resources.length);
|
|
|
|
_.each(resources, (resource) => {
|
|
this._try(resource);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @description Listener which get's called when a resource was added on runtime.
|
|
* @param {String} event
|
|
* @private
|
|
*/
|
|
_onAdded(event) {
|
|
debug('onAdded', this.toString());
|
|
|
|
// CASE: you are type "pages", but the incoming type is "users"
|
|
if (event.type !== this.router.getResourceType()) {
|
|
return;
|
|
}
|
|
|
|
const resource = this.resources.getByIdAndType(event.type, event.id);
|
|
|
|
this._try(resource);
|
|
}
|
|
|
|
/**
|
|
* @description Try to own a resource and generate it's url if so.
|
|
* @param {Resource} resource
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
_try(resource) {
|
|
/**
|
|
* CASE: another url generator has taken this resource already.
|
|
*
|
|
* We have to remember that, because each url generator can generate a different url
|
|
* for a resource. So we can't directly check `this.urls.getUrl(url)`.
|
|
*/
|
|
if (resource.isReserved()) {
|
|
return false;
|
|
}
|
|
|
|
const url = this._generateUrl(resource);
|
|
|
|
// CASE 1: route has no custom filter, it will own the resource for sure
|
|
// CASE 2: find out if my filter matches the resource
|
|
if (!this.filter) {
|
|
this.urls.add({
|
|
url: url,
|
|
generatorId: this.uid,
|
|
resource: resource
|
|
});
|
|
|
|
resource.reserve();
|
|
this._resourceListeners(resource);
|
|
return true;
|
|
} else if (this.nql.queryJSON(resource.data)) {
|
|
this.urls.add({
|
|
url: url,
|
|
generatorId: this.uid,
|
|
resource: resource
|
|
});
|
|
|
|
resource.reserve();
|
|
this._resourceListeners(resource);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Generate url based on the permlink configuration of the target router.
|
|
*
|
|
* @NOTE We currently generate relative urls (https://github.com/TryGhost/Ghost/commit/7b0d5d465ba41073db0c3c72006da625fa11df32).
|
|
*/
|
|
_generateUrl(resource) {
|
|
const permalink = this.router.getPermalinks().getValue();
|
|
return localUtils.replacePermalink(permalink, resource.data);
|
|
}
|
|
|
|
/**
|
|
* @description Helper function to register resource listeners.
|
|
*
|
|
* I want to know if my resources changes.
|
|
*
|
|
* If the owned resource get's updated, we simply release/free the resource and push it back to the queue.
|
|
* This is the easiest, less error prone implementation.
|
|
*
|
|
* Imagine you have two collections: `featured:true` and `page:false`.
|
|
* If a published post status get's featured and you have not explicitly defined `featured:false`, we wouldn't
|
|
* be able to figure out if this resource still belongs to me, because the filter still matches.
|
|
*/
|
|
_resourceListeners(resource) {
|
|
const onUpdate = (updatedResource) => {
|
|
// 1. remove old resource
|
|
this.urls.removeResourceId(updatedResource.data.id);
|
|
|
|
// 2. free resource, the url <-> resource connection no longer exists
|
|
updatedResource.release();
|
|
|
|
// 3. post has the change to get owned from a different collection again
|
|
debug('put back in queue', updatedResource.data.id);
|
|
|
|
this.queue.start({
|
|
event: 'added',
|
|
action: 'added:' + resource.data.id,
|
|
eventData: {
|
|
id: resource.data.id,
|
|
type: this.router.getResourceType()
|
|
}
|
|
});
|
|
};
|
|
|
|
const onRemoved = (removedResource) => {
|
|
this.urls.removeResourceId(removedResource.data.id);
|
|
removedResource.release();
|
|
};
|
|
|
|
resource.removeAllListeners();
|
|
resource.addListener('updated', onUpdate.bind(this));
|
|
resource.addListener('removed', onRemoved.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @description Figure out if this url generator own's a resource id.
|
|
* @param {String} id
|
|
* @returns {boolean}
|
|
*/
|
|
hasId(id) {
|
|
const existingUrl = this.urls.getByResourceId(id);
|
|
|
|
if (existingUrl && existingUrl.generatorId === this.uid) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @description Get all urls of this url generator.
|
|
* @returns {Array}
|
|
*/
|
|
getUrls() {
|
|
return this.urls.getByGeneratorId(this.uid);
|
|
}
|
|
|
|
/**
|
|
* @description Override of `toString`
|
|
* @returns {string}
|
|
*/
|
|
toString() {
|
|
return this.router.toString();
|
|
}
|
|
}
|
|
|
|
module.exports = UrlGenerator;
|