<commit_before>const _ = require('lodash');
const nql = require('@tryghost/nql');
const debug = require('@tryghost/debug')('services:url:generator');
const localUtils = require('../../../shared/url-utils');

// @TODO: merge with filter plugin
const 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 {
    /**
     * @param {Object} options
     * @param {String} options.identifier frontend router ID reference
     * @param {String} options.filter NQL filter string
     * @param {String} options.resourceType resource type (e.g. 'posts', 'tags')
     * @param {String} options.permalink permalink string
     * @param {Object} options.queue instance of the backend Queue
     * @param {Object} options.resources instance of the backend Resources
     * @param {Object} options.urls instance of the backend URLs (used to store the urls)
     * @param {Number} options.position an ID of the generator
     */
    constructor({identifier, filter, resourceType, permalink, queue, resources, urls, position}) {
        this.identifier = identifier;
        this.resourceType = resourceType;
        this.permalink = permalink;
        this.queue = queue;
        this.urls = urls;
        this.resources = resources;
        this.uid = position;

        // CASE: routers can define custom filters, but not required.
        if (filter) {
            this.filter = filter;
            this.nql = nql(this.filter, {
                expansions: EXPANSIONS,
                transformer: nql.utils.mapKeyValues({
                    key: {
                        from: 'page',
                        to: 'type'
                    },
                    values: [{
                        from: false,
                        to: 'post'
                    }, {
                        from: true,
                        to: 'page'
                    }]
                })
            });
            debug('filter', this.filter);
        }

        this._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
     */
    regenerateResources() {
        const myResources = this.urls.getByGeneratorId(this.uid);

        myResources.forEach((object) => {
            this.urls.removeResourceId(object.resource.data.id);
            object.resource.release();
            this._try(object.resource);
        });
    }

    /**
     * @description Helper function to register listeners for each url generator instance.
     * @private
     */
    _listeners() {
        /**
         * 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.resourceType);

        // @NOTE: get the resources of my type e.g. posts.
        const resources = this.resources.getAllByType(this.resourceType);

        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.resourceType) {
            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 {import('./Resource')} resource - instance of the Resource class
     * @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.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) {
        return localUtils.replacePermalink(this.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 `type:page`.
     * 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.<commit_msg>Updated JSDoc and fixed typos<commit_after>const _ = require('lodash');
const nql = require('@tryghost/nql');
const debug = require('@tryghost/debug')('services:url:generator');
const localUtils = require('../../../shared/url-utils');

// @TODO: merge with filter plugin
const 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 {
    /**
     * @param {Object} options
     * @param {String} options.identifier frontend router ID reference
     * @param {String} options.filter NQL filter string
     * @param {String} options.resourceType resource type (e.g. 'posts', 'tags')
     * @param {String} options.permalink permalink string
     * @param {Object} options.queue instance of the backend Queue
     * @param {Object} options.resources instance of the backend Resources
     * @param {Object} options.urls instance of the backend URLs (used to store the urls)
     * @param {Number} options.position an ID of the generator
     */
    constructor({identifier, filter, resourceType, permalink, queue, resources, urls, position}) {
        this.identifier = identifier;
        this.resourceType = resourceType;
        this.permalink = permalink;
        this.queue = queue;
        this.urls = urls;
        this.resources = resources;
        this.uid = position;

        // CASE: routers can define custom filters, but not required.
        if (filter) {
            this.filter = filter;
            this.nql = nql(this.filter, {
                expansions: EXPANSIONS,
                transformer: nql.utils.mapKeyValues({
                    key: {
                        from: 'page',
                        to: 'type'
                    },
                    values: [{
                        from: false,
                        to: 'post'
                    }, {
                        from: true,
                        to: 'page'
                    }]
                })
            });
            debug('filter', this.filter);
        }

        this._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
     */
    regenerateResources() {
        const myResources = this.urls.getByGeneratorId(this.uid);

        myResources.forEach((object) => {
            this.urls.removeResourceId(object.resource.data.id);
            object.resource.release();
            this._try(object.resource);
        });
    }

    /**
     * @description Helper function to register listeners for each url generator instance.
     * @private
     */
    _listeners() {
        /**
         * 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.resourceType);

        // @NOTE: get the resources of my type e.g. posts.
        const resources = this.resources.getAllByType(this.resourceType);

        debug(resources.length);

        _.each(resources, (resource) => {
            this._try(resource);
        });
    }

    /**
     * @description Listener which get's called when a resource was added on runtime.
     * @param {Object} event
     * @param {String} event.type
     * @param {String} event.id
     * @private
     */
    _onAdded(event) {
        debug('onAdded', this.toString());

        // CASE: you are type "pages", but the incoming type is "users"
        if (event.type !== this.resourceType) {
            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 {import('./Resource')} resource - instance of the Resource class
     * @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.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 permalink configuration of the target router.
     *
     * @NOTE We currently generate relative urls (https://github.com/TryGhost/Ghost/commit/7b0d5d465ba41073db0c3c72006da625fa11df32).
     */
    _generateUrl(resource) {
        return localUtils.replacePermalink(this.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 `type:page`.
     * 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.
{'fraction_non_alphanumeric': 0.10570457667558383, 'fraction_numerical': 0.005061094642469814, 'mean_word_length': 1.9945875730677636, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4037692', 'n_tokens_mistral': 3801, 'n_tokens_neox': 3672, 'n_words': 1189}