diff --git a/core/server/helpers/get.js b/core/server/helpers/get.js index affd733c02..1d02de1df5 100644 --- a/core/server/helpers/get.js +++ b/core/server/helpers/get.js @@ -6,12 +6,20 @@ var _ = require('lodash'), Promise = require('bluebird'), errors = require('../errors'), api = require('../api'), + jsonpath = require('jsonpath'), resources, + pathAliases, get; // Endpoints that the helper is able to access resources = ['posts', 'tags', 'users']; +// Short forms of paths which we should understand +pathAliases = { + 'post.tags': 'post.tags[*].slug', + 'post.author': 'post.author.slug' +}; + /** * ## Is Browse * Is this a Browse request or a Read request? @@ -29,6 +37,34 @@ function isBrowse(context, options) { return browse; } +/** + * ## Resolve Paths + * Find and resolve path strings + * + * @param {Object} data + * @param {String} value + * @returns {String} + */ +function resolvePaths(data, value) { + var regex = /\{\{(.*?)\}\}/g; + + value = value.replace(regex, function (match, path) { + var result; + + // Handle aliases + path = pathAliases[path] ? pathAliases[path] : path; + // Handle Handlebars .[] style arrays + path = path.replace(/\.\[/g, '['); + + // Do the query, and convert from array to string + result = jsonpath.query(data, path).join(','); + + return result; + }); + + return value; +} + /** * ## Parse Options * Ensure options passed in make sense @@ -46,6 +82,10 @@ function parseOptions(data, options) { options.author = options.author.slug; } + if (_.isString(options.filter)) { + options.filter = resolvePaths(data, options.filter); + } + return options; } diff --git a/core/test/unit/server_helpers/get_spec.js b/core/test/unit/server_helpers/get_spec.js index 899cb18fbf..df3b21a9f1 100644 --- a/core/test/unit/server_helpers/get_spec.js +++ b/core/test/unit/server_helpers/get_spec.js @@ -9,17 +9,20 @@ var should = require('should'), // Stuff we are testing handlebars = hbs.handlebars, helpers = require('../../../server/helpers'), - api = require('../../../server/api'); + api = require('../../../server/api'), + + sandbox = sinon.sandbox.create(); describe('{{#get}} helper', function () { - var sandbox; + var fn, inverse; before(function () { utils.loadHelpers(); }); beforeEach(function () { - sandbox = sinon.sandbox.create(); + fn = sandbox.spy(); + inverse = sandbox.spy(); }); afterEach(function () { @@ -31,18 +34,19 @@ describe('{{#get}} helper', function () { }); describe('posts', function () { - var testPostsArr = [ - {id: 1, title: 'Test Post 1', author: 'cameron'}, - {id: 2, title: 'Test Post 2', author: 'cameron', featured: true}, + var browseStub, readStub, testPostsArr = [ + {id: 1, title: 'Test Post 1', author: {slug: 'cameron'}}, + {id: 2, title: 'Test Post 2', author: {slug: 'cameron'}, featured: true}, {id: 3, title: 'Test Post 3', tags: [{slug: 'test'}]}, {id: 4, title: 'Test Post 4'} - ]; + ], + meta = {pagination: {}}; beforeEach(function () { - var browseStub = sandbox.stub(api.posts, 'browse'), - readStub = sandbox.stub(api.posts, 'read'); + 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: '3'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 3), meta: meta})); 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: []})); @@ -52,9 +56,6 @@ describe('{{#get}} helper', function () { }); it('should handle default browse posts call', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -71,9 +72,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse posts call with limit 3', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -90,9 +88,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse posts call with limit 1', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -109,9 +104,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse posts call with limit 1', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -128,9 +120,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse post call with explicit tag', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -146,9 +135,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse post call with relative tag', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -164,9 +150,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse post call with explicit author', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -182,9 +165,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse post call with relative author', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -200,9 +180,6 @@ describe('{{#get}} helper', function () { }); it('should handle browse post call with featured:true', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -218,9 +195,6 @@ describe('{{#get}} helper', function () { }); it('should handle read post by id call', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -237,9 +211,6 @@ describe('{{#get}} helper', function () { }); it('should handle empty result set', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -257,9 +228,6 @@ describe('{{#get}} helper', function () { 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', @@ -276,9 +244,6 @@ describe('{{#get}} helper', function () { }); it('should handle error from the API', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts', @@ -295,9 +260,6 @@ describe('{{#get}} helper', function () { }); it('should show warning for call without any options', function (done) { - var fn = sinon.spy(), - inverse = sinon.spy(); - helpers.get.call( {}, 'posts' @@ -309,4 +271,89 @@ describe('{{#get}} helper', function () { }).catch(done); }); }); + + describe('path resolution', function () { + var browseStub, readStub, data = { + post: {id: 3, title: 'Test 3', author: {slug: 'cameron'}, tags: [{slug: 'test'}, {slug: 'magic'}]} + }; + + beforeEach(function () { + browseStub = sandbox.stub(api.posts, 'browse').returns(new Promise.resolve()); + readStub = sandbox.stub(api.posts, 'read').returns(new Promise.resolve()); + }); + + it('should resolve post.tags alias', function (done) { + helpers.get.call( + data, + 'posts', + {hash: {filter: 'tags:[{{post.tags}}]'}, fn: fn, inverse: inverse} + ).then(function () { + browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1); + browseStub.firstCall.args[0].should.be.an.Object.with.property('filter'); + browseStub.firstCall.args[0].filter.should.eql('tags:[test,magic]'); + + done(); + }).catch(done); + }); + + it('should resolve post.author alias', function (done) { + helpers.get.call( + data, + 'posts', + {hash: {filter: 'author:{{post.author}}'}, fn: fn, inverse: inverse} + ).then(function () { + browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1); + browseStub.firstCall.args[0].should.be.an.Object.with.property('filter'); + browseStub.firstCall.args[0].filter.should.eql('author:cameron'); + + done(); + }).catch(done); + }); + + it('should resolve basic path', function (done) { + helpers.get.call( + data, + 'posts', + {hash: {filter: 'id:-{{post.id}}'}, fn: fn, inverse: inverse} + ).then(function () { + browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1); + browseStub.firstCall.args[0].should.be.an.Object.with.property('filter'); + browseStub.firstCall.args[0].filter.should.eql('id:-3'); + + done(); + }).catch(done); + }); + + it('should handle arrays the same as handlebars', function (done) { + var tpl = handlebars.compile('{{post.tags.[0].slug}}'), + output = tpl(data); + + helpers.get.call( + data, + 'posts', + {hash: {filter: 'tags:{{post.tags.[0].slug}}'}, fn: fn, inverse: inverse} + ).then(function () { + browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1); + browseStub.firstCall.args[0].should.be.an.Object.with.property('filter'); + browseStub.firstCall.args[0].filter.should.eql('tags:' + output); + + done(); + }).catch(done); + }); + + it('should output nothing if path does not resolve', function (done) { + helpers.get.call( + data, + 'posts', + {hash: {filter: 'id:{{post.thing}}'}, fn: fn, inverse: inverse} + ).then(function () { + browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1); + browseStub.firstCall.args[0].should.be.an.Object.with.property('filter'); + browseStub.firstCall.args[0].filter.should.eql('id:'); + + done(); + }).catch(done); + }); + }); }); + diff --git a/package.json b/package.json index af2bc3aec3..65278b9c3b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "html-to-text": "1.3.2", "intl": "1.0.0", "intl-messageformat": "1.1.0", + "jsonpath": "0.2.0", "knex": "0.7.3", "lodash": "3.10.1", "moment": "2.10.6",