The {{#get}} helper
closes #4439 - adds basic get helper which works with the current API - allows theme developers to make requests against the API - supports block params and @error message - includes 100% test coverage using posts ---- The `{{#get}}` helper is an asynchronous block helper which allows for making requests for data from the API. This allows theme developers to customise the data which can be shown on a particular page of a blog. Requests can be made to the posts, tags or users API endpoints: ``` {{#get "posts" limit="3"}} {{#foreach posts}} <a href="{{url}}">{{title}}</a> {{/foreach}} {{/get}} ``` The `{{#get}}` helper must be used as a block helper, it supports `{{else}}` logic, for when no data matching the request is available or if an error has occurred: ``` {{#get "posts" tag="photo"}} ... {{else}} {{#if @error}} <p>Something went wrong: {{@error}}</p> {{else}} <p>No posts found</p> {{/if}} {{/get}} ``` The helper also supports block params, meaning the data it outputs can be given a different name: ``` {{#get "posts" featured="true" as |featured|}} {{#foreach featured}} ... {{/foreach}} {{/get}} ``` Please Note: At present asynchronous helpers cannot be nested.
This commit is contained in:
parent
4c1828c027
commit
3e40637cd4
101
core/server/helpers/get.js
Normal file
101
core/server/helpers/get.js
Normal file
@ -0,0 +1,101 @@
|
||||
// # Get Helper
|
||||
// Usage: `{{#get "posts" limit="5"}}`, `{{#get "tags" limit="all"}}`
|
||||
// Fetches data from the API
|
||||
var _ = require('lodash'),
|
||||
hbs = require('express-hbs'),
|
||||
Promise = require('bluebird'),
|
||||
errors = require('../errors'),
|
||||
api = require('../api'),
|
||||
resources,
|
||||
get;
|
||||
|
||||
// Endpoints that the helper is able to access
|
||||
resources = ['posts', 'tags', 'users'];
|
||||
|
||||
/**
|
||||
* ## Is Browse
|
||||
* Is this a Browse request or a Read request?
|
||||
* @param {Object} context
|
||||
* @param {Object} options
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isBrowse(context, options) {
|
||||
var browse = true;
|
||||
|
||||
if (options.id || options.slug) {
|
||||
browse = false;
|
||||
}
|
||||
|
||||
return browse;
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Parse Options
|
||||
* Ensure options passed in make sense
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
* @returns {*}
|
||||
*/
|
||||
function parseOptions(data, options) {
|
||||
if (_.isArray(options.tag)) {
|
||||
options.tag = _.pluck(options.tag, 'slug').join(',');
|
||||
}
|
||||
|
||||
if (_.isObject(options.author)) {
|
||||
options.author = options.author.slug;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Get
|
||||
* @param {Object} context
|
||||
* @param {Object} options
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get = function get(context, options) {
|
||||
options = options || {};
|
||||
options.hash = options.hash || {};
|
||||
options.data = options.data || {};
|
||||
|
||||
var self = this,
|
||||
data = hbs.handlebars.createFrame(options.data),
|
||||
apiOptions = _.omit(options.hash, 'context'),
|
||||
apiMethod;
|
||||
|
||||
if (!options.fn) {
|
||||
data.error = 'Get helper must be called as a block';
|
||||
errors.logWarn(data.error);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!_.contains(resources, context)) {
|
||||
data.error = 'Invalid resource given to get helper';
|
||||
errors.logWarn(data.error);
|
||||
return Promise.resolve(options.inverse(self, {data: data}));
|
||||
}
|
||||
|
||||
// Determine if this is a read or browse
|
||||
apiMethod = isBrowse(context, apiOptions) ? api[context].browse : api[context].read;
|
||||
// Parse the options we're going to pass to the API
|
||||
apiOptions = parseOptions(this, apiOptions);
|
||||
|
||||
return apiMethod(apiOptions).then(function success(result) {
|
||||
result = _.merge(self, result);
|
||||
if (_.isEmpty(result[context])) {
|
||||
return options.inverse(self, {data: data});
|
||||
}
|
||||
|
||||
return options.fn(result, {
|
||||
data: data,
|
||||
blockParams: [result[context]]
|
||||
});
|
||||
}).catch(function error(err) {
|
||||
data.error = err.message;
|
||||
return options.inverse(self, {data: data});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = get;
|
@ -21,6 +21,7 @@ coreHelpers.date = require('./date');
|
||||
coreHelpers.encode = require('./encode');
|
||||
coreHelpers.excerpt = require('./excerpt');
|
||||
coreHelpers.foreach = require('./foreach');
|
||||
coreHelpers.get = require('./get');
|
||||
coreHelpers.ghost_foot = require('./ghost_foot');
|
||||
coreHelpers.ghost_head = require('./ghost_head');
|
||||
coreHelpers.image = require('./image');
|
||||
@ -117,6 +118,7 @@ registerHelpers = function (adminHbs) {
|
||||
registerAsyncThemeHelper('post_class', coreHelpers.post_class);
|
||||
registerAsyncThemeHelper('next_post', coreHelpers.next_post);
|
||||
registerAsyncThemeHelper('prev_post', coreHelpers.prev_post);
|
||||
registerAsyncThemeHelper('get', coreHelpers.get);
|
||||
|
||||
// Register admin helpers
|
||||
registerAdminHelper('asset', coreHelpers.asset);
|
||||
|
312
core/test/unit/server_helpers/get_spec.js
Normal file
312
core/test/unit/server_helpers/get_spec.js
Normal file
@ -0,0 +1,312 @@
|
||||
/*globals describe, before, beforeEach, afterEach, it*/
|
||||
/*jshint expr:true*/
|
||||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
hbs = require('express-hbs'),
|
||||
Promise = require('bluebird'),
|
||||
utils = require('./utils'),
|
||||
|
||||
// Stuff we are testing
|
||||
handlebars = hbs.handlebars,
|
||||
helpers = require('../../../server/helpers'),
|
||||
api = require('../../../server/api');
|
||||
|
||||
describe('{{#get}} helper', function () {
|
||||
var sandbox;
|
||||
|
||||
before(function () {
|
||||
utils.loadHelpers();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('has loaded get block helper', function () {
|
||||
should.exist(handlebars.helpers.get);
|
||||
});
|
||||
|
||||
describe('posts', function () {
|
||||
var testPostsArr = [
|
||||
{id: 1, title: 'Test Post 1', author: 'cameron'},
|
||||
{id: 2, title: 'Test Post 2', author: 'cameron', featured: true},
|
||||
{id: 3, title: 'Test Post 3', tags: [{slug: 'test'}]},
|
||||
{id: 4, title: 'Test Post 4'}
|
||||
];
|
||||
beforeEach(function () {
|
||||
var browseStub = sandbox.stub(api.posts, 'browse'),
|
||||
readStub = sandbox.stub(api.posts, 'read');
|
||||
|
||||
browseStub.returns(new Promise.resolve({posts: testPostsArr}));
|
||||
browseStub.withArgs({limit: '3'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 3)}));
|
||||
browseStub.withArgs({limit: '1'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 1)}));
|
||||
browseStub.withArgs({tag: 'test'}).returns(new Promise.resolve({posts: testPostsArr.slice(2, 3)}));
|
||||
browseStub.withArgs({tag: 'none'}).returns(new Promise.resolve({posts: []}));
|
||||
browseStub.withArgs({author: 'cameron'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 2)}));
|
||||
browseStub.withArgs({featured: 'true'}).returns(new Promise.resolve({posts: testPostsArr.slice(2, 3)}));
|
||||
readStub.withArgs({id: '2'}).returns(new Promise.resolve({posts: testPostsArr.slice(1, 2)}));
|
||||
});
|
||||
|
||||
it('should handle default browse posts call', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.called.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr);
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(4);
|
||||
inverse.called.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse posts call with limit 3', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {limit: '3'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(3);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(0, 3));
|
||||
inverse.called.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse posts call with limit 1', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {limit: '1'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(1);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(0, 1));
|
||||
inverse.called.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse posts call with limit 1', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {limit: '1'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(1);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(0, 1));
|
||||
inverse.called.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse post call with explicit tag', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {tag: 'test'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(1);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(2, 3));
|
||||
inverse.called.should.be.false;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse post call with relative tag', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {tag: [{slug: 'test'}]}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(1);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(2, 3));
|
||||
inverse.called.should.be.false;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse post call with explicit author', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {author: 'cameron'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(2);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(0, 2));
|
||||
inverse.called.should.be.false;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse post call with relative author', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {author: {slug: 'cameron'}}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(2);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(0, 2));
|
||||
inverse.called.should.be.false;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle browse post call with featured:true', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {featured: 'true'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(1);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(2, 3));
|
||||
inverse.called.should.be.false;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle read post by id call', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {id: '2'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.calledOnce.should.be.true;
|
||||
fn.firstCall.args[0].should.be.an.Object.with.property('posts');
|
||||
fn.firstCall.args[0].posts.should.have.lengthOf(1);
|
||||
fn.firstCall.args[0].posts.should.eql(testPostsArr.slice(1, 2));
|
||||
inverse.called.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle empty result set', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {tag: 'none'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.called.should.be.false;
|
||||
inverse.calledOnce.should.be.true;
|
||||
inverse.firstCall.args[1].should.be.an.Object.and.have.property('data');
|
||||
inverse.firstCall.args[1].data.should.be.an.Object.and.not.have.property('error');
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('general error handling', function () {
|
||||
it('should return an error for an unknown resource', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'magic',
|
||||
{hash: {}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.called.should.be.false;
|
||||
inverse.calledOnce.should.be.true;
|
||||
inverse.firstCall.args[1].should.be.an.Object.and.have.property('data');
|
||||
inverse.firstCall.args[1].data.should.be.an.Object.and.have.property('error');
|
||||
inverse.firstCall.args[1].data.error.should.eql('Invalid resource given to get helper');
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should handle error from the API', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts',
|
||||
{hash: {tag: 'thing!'}, fn: fn, inverse: inverse}
|
||||
).then(function () {
|
||||
fn.called.should.be.false;
|
||||
inverse.calledOnce.should.be.true;
|
||||
inverse.firstCall.args[1].should.be.an.Object.and.have.property('data');
|
||||
inverse.firstCall.args[1].data.should.be.an.Object.and.have.property('error');
|
||||
inverse.firstCall.args[1].data.error.should.match(/^Validation/);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should show warning for call without any options', function (done) {
|
||||
var fn = sinon.spy(),
|
||||
inverse = sinon.spy();
|
||||
|
||||
helpers.get.call(
|
||||
{},
|
||||
'posts'
|
||||
).then(function () {
|
||||
fn.called.should.be.false;
|
||||
inverse.called.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user