mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 09:52:06 +03:00
0c853a372b
refs https://github.com/TryGhost/Team/issues/1251 With sites that have a huge number of resources, using limit="all" can cause OOM errors at the Node level. Administrators now have the ability to cap limit="all" requests via config. This only affects the get helper used in themes, not the API, this is by design as themes have less visibility of issues.
203 lines
5.9 KiB
JavaScript
203 lines
5.9 KiB
JavaScript
// # Get Helper
|
|
// Usage: `{{#get "posts" limit="5"}}`, `{{#get "tags" limit="all"}}`
|
|
// Fetches data from the API
|
|
const {config, api, prepareContextResource} = require('../services/proxy');
|
|
const {hbs} = require('../services/rendering');
|
|
|
|
const logging = require('@tryghost/logging');
|
|
const errors = require('@tryghost/errors');
|
|
const tpl = require('@tryghost/tpl');
|
|
|
|
const _ = require('lodash');
|
|
const Promise = require('bluebird');
|
|
const jsonpath = require('jsonpath');
|
|
|
|
const messages = {
|
|
mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
|
|
invalidResource: 'Invalid resource given to get helper'
|
|
};
|
|
|
|
const createFrame = hbs.handlebars.createFrame;
|
|
|
|
const RESOURCES = {
|
|
posts: {
|
|
alias: 'postsPublic'
|
|
},
|
|
tags: {
|
|
alias: 'tagsPublic'
|
|
},
|
|
pages: {
|
|
alias: 'pagesPublic'
|
|
},
|
|
authors: {
|
|
alias: 'authorsPublic'
|
|
}
|
|
};
|
|
|
|
// Short forms of paths which we should understand
|
|
const pathAliases = {
|
|
'post.tags': 'post.tags[*].slug',
|
|
'post.author': 'post.author.slug'
|
|
};
|
|
|
|
/**
|
|
* ## Is Browse
|
|
* Is this a Browse request or a Read request?
|
|
* @param {Object} resource
|
|
* @param {Object} options
|
|
* @returns {boolean}
|
|
*/
|
|
function isBrowse(options) {
|
|
let browse = true;
|
|
|
|
if (options.id || options.slug) {
|
|
browse = false;
|
|
}
|
|
|
|
return browse;
|
|
}
|
|
|
|
/**
|
|
* ## Resolve Paths
|
|
* Find and resolve path strings
|
|
*
|
|
* @param {Object} data
|
|
* @param {String} value
|
|
* @returns {String}
|
|
*/
|
|
function resolvePaths(globals, data, value) {
|
|
const regex = /\{\{(.*?)\}\}/g;
|
|
|
|
value = value.replace(regex, function (match, path) {
|
|
let result;
|
|
|
|
// Handle aliases
|
|
path = pathAliases[path] ? pathAliases[path] : path;
|
|
// Handle Handlebars .[] style arrays
|
|
path = path.replace(/\.\[/g, '[');
|
|
|
|
if (path.charAt(0) === '@') {
|
|
result = jsonpath.query(globals, path.substr(1));
|
|
} else {
|
|
// Do the query, which always returns an array of matches
|
|
result = jsonpath.query(data, path);
|
|
}
|
|
|
|
// Handle the case where the single data property we return is a Date
|
|
// Data.toString() is not DB compatible, so use `toISOString()` instead
|
|
if (_.isDate(result[0])) {
|
|
result[0] = result[0].toISOString();
|
|
}
|
|
|
|
// Concatenate the results with a comma, handles common case of multiple tag slugs
|
|
return result.join(',');
|
|
});
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* ## Parse Options
|
|
* Ensure options passed in make sense
|
|
*
|
|
* @param {Object} data
|
|
* @param {Object} options
|
|
* @returns {*}
|
|
*/
|
|
function parseOptions(globals, data, options) {
|
|
if (_.isString(options.filter)) {
|
|
options.filter = resolvePaths(globals, data, options.filter);
|
|
}
|
|
|
|
if (options.limit === 'all' && config.get('getHelperLimitAllMax')) {
|
|
options.limit = config.get('getHelperLimitAllMax');
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* ## Get
|
|
* @param {Object} resource
|
|
* @param {Object} options
|
|
* @returns {Promise}
|
|
*/
|
|
module.exports = function get(resource, options) {
|
|
options = options || {};
|
|
options.hash = options.hash || {};
|
|
options.data = options.data || {};
|
|
|
|
const self = this;
|
|
const start = Date.now();
|
|
const data = createFrame(options.data);
|
|
const ghostGlobals = _.omit(data, ['_parent', 'root']);
|
|
const apiVersion = _.get(data, 'root._locals.apiVersion');
|
|
let apiOptions = options.hash;
|
|
let returnedRowsCount;
|
|
|
|
if (!options.fn) {
|
|
data.error = tpl(messages.mustBeCalledAsBlock, {helperName: 'get'});
|
|
logging.warn(data.error);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (!RESOURCES[resource]) {
|
|
data.error = tpl(messages.invalidResource);
|
|
logging.warn(data.error);
|
|
return Promise.resolve(options.inverse(self, {data: data}));
|
|
}
|
|
|
|
const controllerName = RESOURCES[resource].alias;
|
|
const controller = api[apiVersion][controllerName];
|
|
const action = isBrowse(apiOptions) ? 'browse' : 'read';
|
|
|
|
// Parse the options we're going to pass to the API
|
|
apiOptions = parseOptions(ghostGlobals, this, apiOptions);
|
|
|
|
// @TODO: https://github.com/TryGhost/Ghost/issues/10548
|
|
return controller[action](apiOptions).then(function success(result) {
|
|
// prepare data properties for use with handlebars
|
|
if (result[resource] && result[resource].length) {
|
|
result[resource].forEach(prepareContextResource);
|
|
}
|
|
|
|
// used for logging details of slow requests
|
|
returnedRowsCount = result[resource] && result[resource].length;
|
|
|
|
// block params allows the theme developer to name the data using something like
|
|
// `{{#get "posts" as |result pageInfo|}}`
|
|
const blockParams = [result[resource]];
|
|
if (result.meta && result.meta.pagination) {
|
|
result.pagination = result.meta.pagination;
|
|
blockParams.push(result.meta.pagination);
|
|
}
|
|
|
|
// Call the main template function
|
|
return options.fn(result, {
|
|
data: data,
|
|
blockParams: blockParams
|
|
});
|
|
}).catch(function error(err) {
|
|
logging.error(err);
|
|
data.error = err.message;
|
|
return options.inverse(self, {data: data});
|
|
}).finally(function () {
|
|
const totalMs = Date.now() - start;
|
|
const logLevel = config.get('logging:slowHelper:level');
|
|
const threshold = config.get('logging:slowHelper:threshold');
|
|
if (totalMs > threshold) {
|
|
logging[logLevel](new errors.HelperWarning({
|
|
message: `{{#get}} helper took ${totalMs}ms to complete`,
|
|
code: 'SLOW_GET_HELPER',
|
|
errorDetails: {
|
|
api: `${apiVersion}.${controllerName}.${action}`,
|
|
apiOptions,
|
|
returnedRows: returnedRowsCount
|
|
}
|
|
}));
|
|
}
|
|
});
|
|
};
|
|
|
|
module.exports.async = true;
|