Skip to content

Commit

Permalink
✨ Add ?formats param to Posts API (#8305)
Browse files Browse the repository at this point in the history
refs #8275
- Adds support for `formats` param
- Returns `html` by default
- Can optionally return other formats by providing a comma-separated list
  • Loading branch information
ErisDS authored and kevinansfield committed May 30, 2017
1 parent 25c4e50 commit 3e60941
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 17 deletions.
8 changes: 4 additions & 4 deletions core/server/api/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ posts = {
* @returns {Promise<Posts>} Posts Collection with Meta
*/
browse: function browse(options) {
var extraOptions = ['status'],
var extraOptions = ['status', 'formats'],
permittedOptions,
tasks;

Expand All @@ -62,7 +62,7 @@ posts = {
tasks = [
utils.validate(docName, {opts: permittedOptions}),
utils.handlePublicPermissions(docName, 'browse'),
utils.convertOptions(allowedIncludes),
utils.convertOptions(allowedIncludes, dataProvider.Post.allowedFormats),
modelQuery
];

Expand All @@ -79,7 +79,7 @@ posts = {
* @return {Promise<Post>} Post
*/
read: function read(options) {
var attrs = ['id', 'slug', 'status', 'uuid'],
var attrs = ['id', 'slug', 'status', 'uuid', 'formats'],
tasks;

/**
Expand All @@ -96,7 +96,7 @@ posts = {
tasks = [
utils.validate(docName, {attrs: attrs, opts: options.opts || []}),
utils.handlePublicPermissions(docName, 'read'),
utils.convertOptions(allowedIncludes),
utils.convertOptions(allowedIncludes, dataProvider.Post.allowedFormats),
modelQuery
];

Expand Down
23 changes: 18 additions & 5 deletions core/server/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ utils = {
name: {}
},
// these values are sanitised/validated separately
noValidation = ['data', 'context', 'include', 'filter', 'forUpdate', 'transacting'],
noValidation = ['data', 'context', 'include', 'filter', 'forUpdate', 'transacting', 'formats'],
errors = [];

_.each(options, function (value, key) {
Expand Down Expand Up @@ -243,12 +243,16 @@ utils = {
return this.trimAndLowerCase(fields);
},

prepareFormats: function prepareFormats(formats, allowedFormats) {
return _.intersection(this.trimAndLowerCase(formats), allowedFormats);
},

/**
* ## Convert Options
* @param {Array} allowedIncludes
* @returns {Function} doConversion
*/
convertOptions: function convertOptions(allowedIncludes) {
convertOptions: function convertOptions(allowedIncludes, allowedFormats) {
/**
* Convert our options from API-style to Model-style
* @param {Object} options
Expand All @@ -258,11 +262,20 @@ utils = {
if (options.include) {
options.include = utils.prepareInclude(options.include, allowedIncludes);
}

if (options.fields) {
options.columns = utils.prepareFields(options.fields);
delete options.fields;
}

if (options.formats) {
options.formats = utils.prepareFormats(options.formats, allowedFormats);
}

if (options.formats && options.columns) {
options.columns = options.columns.concat(options.formats);
}

return options;
};
},
Expand All @@ -274,7 +287,7 @@ utils = {
* @param {String} docName
* @returns {Promise(Object)} resolves to the original object if it checks out
*/
checkObject: function (object, docName, editId) {
checkObject: function checkObject(object, docName, editId) {
if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) {
return Promise.reject(new errors.BadRequestError({
message: i18n.t('errors.api.utils.noRootKeyProvided', {docName: docName})
Expand Down Expand Up @@ -306,10 +319,10 @@ utils = {

return Promise.resolve(object);
},
checkFileExists: function (fileData) {
checkFileExists: function checkFileExists(fileData) {
return !!(fileData.mimetype && fileData.path);
},
checkFileIsValid: function (fileData, types, extensions) {
checkFileIsValid: function checkFileIsValid(fileData, types, extensions) {
var type = fileData.mimetype,
ext = path.extname(fileData.name).toLowerCase();

Expand Down
2 changes: 1 addition & 1 deletion core/server/data/schema/checks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
function isPost(jsonData) {
return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown') &&
return jsonData.hasOwnProperty('html') &&
jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug');
}

Expand Down
24 changes: 24 additions & 0 deletions core/server/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,12 +477,31 @@ Post = ghostBookshelf.Model.extend({
defaultColumnsToFetch: function defaultColumnsToFetch() {
return ['id', 'published_at', 'slug', 'author_id'];
},
/**
* If the `formats` option is not used, we return `html` be default.
* Otherwise we return what is requested e.g. `?formats=mobiledoc,plaintext`
*/
formatsToJSON: function formatsToJSON(attrs, options) {
var defaultFormats = ['html'],
formatsToKeep = options.formats || defaultFormats;

// Iterate over all known formats, and if they are not in the keep list, remove them
_.each(Post.allowedFormats, function (format) {
if (formatsToKeep.indexOf(format) === -1) {
delete attrs[format];
}
});

return attrs;
},

toJSON: function toJSON(options) {
options = options || {};

var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);

attrs = this.formatsToJSON(attrs, options);

if (!options.columns || (options.columns && options.columns.indexOf('author') > -1)) {
attrs.author = attrs.author || attrs.author_id;
delete attrs.author_id;
Expand All @@ -505,6 +524,8 @@ Post = ghostBookshelf.Model.extend({
return this.isPublicContext() ? 'page:false' : 'page:false+status:published';
}
}, {
allowedFormats: ['markdown', 'mobiledoc', 'html', 'plaintext', 'amp'],

orderDefaultOptions: function orderDefaultOptions() {
return {
status: 'ASC',
Expand Down Expand Up @@ -580,6 +601,9 @@ Post = ghostBookshelf.Model.extend({
edit: ['forUpdate']
};

// The post model additionally supports having a formats option
options.push('formats');

if (validOptions[methodName]) {
options = options.concat(validOptions[methodName]);
}
Expand Down
132 changes: 132 additions & 0 deletions core/test/functional/routes/api/posts_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,138 @@ describe('Post API', function () {
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);

done();
});
});

it('can retrieve a single post format', function (done) {
request.get(testUtils.API.getApiQuery('posts/?formats=mobiledoc'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}

should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(5);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc'], ['html']);
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);

done();
});
});

it('can retrieve multiple post formats', function (done) {
request.get(testUtils.API.getApiQuery('posts/?formats=plaintext,mobiledoc,amp'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}

should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(5);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext', 'amp'], ['html']);
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);

done();
});
});

it('can handle unknown post formats', function (done) {
request.get(testUtils.API.getApiQuery('posts/?formats=plaintext,mobiledo'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}

should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(5);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['plaintext'], ['html']);
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);

done();
});
});

it('can handle empty formats (default html is expected)', function (done) {
request.get(testUtils.API.getApiQuery('posts/?formats='))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}

should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(5);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);

done();
});
});

it('fields and formats', function (done) {
request.get(testUtils.API.getApiQuery('posts/?formats=mobiledoc,html&fields=id,title'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}

should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(5);

testUtils.API.checkResponse(
jsonResponse.posts[0],
'post',
null,
null,
['mobiledoc', 'id', 'title', 'html']
);

testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');

done();
});
});
Expand Down
50 changes: 46 additions & 4 deletions core/test/integration/model/model_posts_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ describe('Post Model', function () {

beforeEach(testUtils.setup('owner', 'posts', 'apps'));

function checkFirstPostData(firstPost) {
function checkFirstPostData(firstPost, options) {
options = options || {};

should.not.exist(firstPost.author_id);
firstPost.author.should.be.an.Object();
firstPost.url.should.equal('/html-ipsum/');
Expand All @@ -60,10 +62,29 @@ describe('Post Model', function () {
firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.tags[0].name.should.equal(DataGenerator.Content.tags[0].name);

// Formats
// @TODO change / update this for mobiledoc in
firstPost.markdown.should.match(/HTML Ipsum Presents/);
firstPost.html.should.match(/HTML Ipsum Presents/);
if (options.formats) {
if (options.formats.indexOf('markdown') !== -1) {
firstPost.markdown.should.match(/HTML Ipsum Presents/);
}

if (options.formats.indexOf('html') !== -1) {
firstPost.html.should.match(/HTML Ipsum Presents/);
}

if (options.formats.indexOf('plaintext') !== -1) {
/**
* NOTE: this is null, not undefined, so it was returned
* The plaintext value is generated.
*/
should.equal(firstPost.plaintext, null);
}
} else {
firstPost.html.should.match(/HTML Ipsum Presents/);
should.equal(firstPost.plaintext, undefined);
should.equal(firstPost.markdown, undefined);
should.equal(firstPost.amp, undefined);
}
}

describe('findAll', function () {
Expand Down Expand Up @@ -98,6 +119,27 @@ describe('Post Model', function () {
done();
}).catch(done);
});

it('can findAll, use formats option', function (done) {
var options = {
formats: ['markdown', 'plaintext'],
include: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']
};

PostModel.findAll(options)
.then(function (results) {
should.exist(results);
results.length.should.be.above(0);

var posts = results.models.map(function (model) {
return model.toJSON(options);
}), firstPost = _.find(posts, {title: testUtils.DataGenerator.Content.posts[0].title});

checkFirstPostData(firstPost, options);

done();
}).catch(done);
});
});

describe('findPage', function () {
Expand Down
Loading

0 comments on commit 3e60941

Please sign in to comment.