Added collections helper

This is an initial implementation which uses the Posts Content API rather than
the Collections Content API, this is because we haven't added the Collections
Content API yet, but we can added it later when necessary.
This commit is contained in:
Fabien "egg" O'Carroll 2023-07-17 10:47:37 +01:00 committed by Fabien 'egg' O'Carroll
parent be0dbe3e91
commit 605aa18d6a
2 changed files with 146 additions and 1 deletions

View File

@ -0,0 +1,145 @@
// # 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/handlebars');
const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const messages = {
mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
invalidResource: 'Invalid "{resource}" resource given to get helper'
};
const createFrame = hbs.handlebars.createFrame;
/**
* ## Parse Options
* Ensure options passed in make sense
*
* @param {Object} options
* @returns {*}
*/
function parseOptions(options) {
if (options.limit === 'all' || !options.limit) {
return {
limit: 3
};
}
return {
limit: options.limit
};
}
/**
*
* @param {String} resource
* @param {String} controllerName
* @param {String} action
* @param {Object} apiOptions
* @returns {Promise<Object>}
*/
async function makeAPICall(resource, controllerName, action, apiOptions) {
const controller = api[controllerName];
let timer;
try {
let response;
if (config.get('optimization:getHelper:timeout:threshold')) {
const logLevel = config.get('optimization:getHelper:timeout:level') || 'error';
const threshold = config.get('optimization:getHelper:timeout:threshold');
const apiResponse = controller[action](apiOptions);
const timeout = new Promise((resolve) => {
timer = setTimeout(() => {
logging[logLevel](new errors.HelperWarning({
message: `{{#get}} took longer than ${threshold}ms and was aborted`,
code: 'ABORTED_GET_HELPER',
errorDetails: {
api: `${controllerName}.${action}`,
apiOptions
}
}));
resolve({[resource]: []});
}, threshold);
});
response = await Promise.race([apiResponse, timeout]);
clearTimeout(timer);
} else {
response = await controller[action](apiOptions);
}
return response;
} catch (err) {
clearTimeout(timer);
throw err;
}
}
/**
* ## Get
* @param {string} slug
* @param {object} options
* @returns {Promise<any>}
*/
module.exports = async function collection(slug, options) {
options = options || {};
options.hash = options.hash || {};
options.data = options.data || {};
const self = this;
const data = createFrame(options.data);
let apiOptions = options.hash;
if (!options.fn) {
data.error = tpl(messages.mustBeCalledAsBlock, {helperName: 'collection'});
logging.warn(data.error);
return;
}
const resource = 'posts';
const controllerName = 'postsPublic';
const action = 'browse';
// Parse the options we're going to pass to the API
apiOptions = parseOptions(apiOptions);
apiOptions.context = {member: data.member};
try {
const response = await makeAPICall(resource, controllerName, action, apiOptions);
// prepare data properties for use with handlebars
if (response[resource] && response[resource].length) {
response[resource].forEach(prepareContextResource);
}
// block params allows the theme developer to name the data using something like
// `{{#get "posts" as |result pageInfo|}}`
const blockParams = [response[resource]];
if (response.meta && response.meta.pagination) {
response.pagination = response.meta.pagination;
blockParams.push(response.meta.pagination);
}
// Call the main template function
return options.fn(response, {
data: data,
blockParams: blockParams
});
} catch (error) {
logging.error(error);
data.error = error.message;
return options.inverse(self, {data: data});
}
};
module.exports.async = true;

View File

@ -11,7 +11,7 @@ describe('Helpers', function () {
'asset', 'authors', 'body_class', 'cancel_link', 'concat', 'content', 'date', 'encode', 'excerpt', 'facebook_url', 'foreach', 'get', 'asset', 'authors', 'body_class', 'cancel_link', 'concat', 'content', 'date', 'encode', 'excerpt', 'facebook_url', 'foreach', 'get',
'ghost_foot', 'ghost_head', 'has', 'img_url', 'is', 'link', 'link_class', 'meta_description', 'meta_title', 'navigation', 'ghost_foot', 'ghost_head', 'has', 'img_url', 'is', 'link', 'link_class', 'meta_description', 'meta_title', 'navigation',
'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'price', 'raw', 'reading_time', 't', 'tags', 'title','total_members', 'total_paid_members', 'twitter_url', 'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'price', 'raw', 'reading_time', 't', 'tags', 'title','total_members', 'total_paid_members', 'twitter_url',
'url', 'comment_count' 'url', 'comment_count', 'collection'
]; ];
const experimentalHelpers = ['match', 'tiers', 'comments', 'search']; const experimentalHelpers = ['match', 'tiers', 'comments', 'search'];