Skip to content

Commit

Permalink
Refactor to using pipeline for the API
Browse files Browse the repository at this point in the history
refs #2758

- Post, Tag & User API methods are refactored to use pipeline
- Each functional code block is a named task function
- Each function takes options, manipulates it, and returns options back
- Tasks like permissions can reject if they don't pass, causing the pipeline to fail
- Tasks like validating and converting options might be abstracted out into utils - the same for each endpoint
- Tasks like the data call can be extremely complex if needs be (like for some user endpoints)
- Option validation is mostly factored out to utils
- Option conversion is factored out to utils
- API utils have 100% test coverage
- Minor updates to inline docs, more to do here
  • Loading branch information
ErisDS committed Jun 28, 2015
1 parent 09402d2 commit 51ac3f6
Show file tree
Hide file tree
Showing 5 changed files with 991 additions and 336 deletions.
263 changes: 178 additions & 85 deletions core/server/api/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,79 +6,107 @@ var Promise = require('bluebird'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
utils = require('./utils'),
pipeline = require('../utils/pipeline'),

docName = 'posts',
allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields', 'next', 'previous'],
posts;

// ## Helpers
function prepareInclude(include) {
include = include || '';
include = _.intersection(include.split(','), allowedIncludes);

return include;
}

/**
* ## Posts API Methods
* ### Posts API Methods
*
* **See:** [API Methods](index.js.html#api%20methods)
*/
posts = {

/**
* ### Browse
* ## Browse
* Find a paginated set of posts
*
* Will only return published posts unless we have an authenticated user and an alternative status
* parameter.
*
* Will return without static pages unless told otherwise
*
* Can return posts for a particular tag by passing a tag slug in
*
* @public
* @param {{context, page, limit, status, staticPages, tag, featured}} options (optional)
* @returns {Promise(Posts)} Posts Collection with Meta
* @returns {Promise<Posts>} Posts Collection with Meta
*/
browse: function browse(options) {
options = options || {};
var tasks;

if (!(options.context && options.context.user)) {
options.status = 'published';
/**
* ### Handle Permissions
* We need to either be an authorised user, or only return published posts.
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
if (!(options.context && options.context.user)) {
options.status = 'published';
}

return options;
}

if (options.include) {
options.include = prepareInclude(options.include);
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function modelQuery(options) {
return dataProvider.Post.findPage(options);
}

return dataProvider.Post.findPage(options);
// Push all of our tasks into a `tasks` array in the correct order
tasks = [utils.validate(docName), handlePermissions, utils.convertOptions(allowedIncludes), modelQuery];

// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options);
},

/**
* ### Read
* ## Read
* Find a post, by ID, UUID, or Slug
*
* @public
* @param {{id_or_slug (required), context, status, include, ...}} options
* @return {Promise(Post)} Post
* @param {Object} options
* @return {Promise<Post>} Post
*/
read: function read(options) {
var attrs = ['id', 'slug', 'status', 'uuid'],
data = _.pick(options, attrs);

options = _.omit(options, attrs);
tasks;

// only published posts if no user is present
if (!data.uuid && !(options.context && options.context.user)) {
data.status = 'published';
/**
* ### Handle Permissions
* We need to either be an authorised user, or only return published posts.
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
if (!options.data.uuid && !(options.context && options.context.user)) {
options.data.status = 'published';
}
return options;
}

if (options.include) {
options.include = prepareInclude(options.include);
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function modelQuery(options) {
return dataProvider.Post.findOne(options.data, _.omit(options, ['data']));
}

return dataProvider.Post.findOne(data, options).then(function (result) {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [utils.validate(docName, attrs), handlePermissions, utils.convertOptions(allowedIncludes), modelQuery];

// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options).then(function formatResponse(result) {
// @TODO make this a formatResponse task?
if (result) {
return {posts: [result.toJSON(options)]};
}
Expand All @@ -88,7 +116,7 @@ posts = {
},

/**
* ### Edit
* ## Edit
* Update properties of a post
*
* @public
Expand All @@ -97,34 +125,54 @@ posts = {
* @return {Promise(Post)} Edited Post
*/
edit: function edit(object, options) {
return canThis(options.context).edit.post(options.id).then(function () {
return utils.checkObject(object, docName, options.id).then(function (checkedPostData) {
if (options.include) {
options.include = prepareInclude(options.include);
}
var tasks;

/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).edit.post(options.id).then(function permissionGranted() {
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error, 'You do not have permission to edit posts.');
});
}

return dataProvider.Post.edit(checkedPostData.posts[0], options);
}).then(function (result) {
if (result) {
var post = result.toJSON(options);

// If previously was not published and now is (or vice versa), signal the change
post.statusChanged = false;
if (result.updated('status') !== result.get('status')) {
post.statusChanged = true;
}
return {posts: [post]};
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function modelQuery(options) {
return dataProvider.Post.edit(options.data.posts[0], _.omit(options, ['data']));
}

// Push all of our tasks into a `tasks` array in the correct order
tasks = [utils.validate(docName), handlePermissions, utils.convertOptions(allowedIncludes), modelQuery];

// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, object, options).then(function formatResponse(result) {
if (result) {
var post = result.toJSON(options);

// If previously was not published and now is (or vice versa), signal the change
post.statusChanged = false;
if (result.updated('status') !== result.get('status')) {
post.statusChanged = true;
}
return {posts: [post]};
}

return Promise.reject(new errors.NotFoundError('Post not found.'));
});
}, function () {
return Promise.reject(new errors.NoPermissionError('You do not have permission to edit posts.'));
return Promise.reject(new errors.NotFoundError('Post not found.'));
});
},

/**
* ### Add
* ## Add
* Create a new post along with any tags
*
* @public
Expand All @@ -133,58 +181,103 @@ posts = {
* @return {Promise(Post)} Created Post
*/
add: function add(object, options) {
options = options || {};
var tasks;

return canThis(options.context).add.post().then(function () {
return utils.checkObject(object, docName).then(function (checkedPostData) {
if (options.include) {
options.include = prepareInclude(options.include);
}
/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).add.post().then(function permissionGranted() {
return options;
}).catch(function () {
return Promise.reject(new errors.NoPermissionError('You do not have permission to add posts.'));
});
}

return dataProvider.Post.add(checkedPostData.posts[0], options);
}).then(function (result) {
var post = result.toJSON(options);
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function modelQuery(options) {
return dataProvider.Post.add(options.data.posts[0], _.omit(options, ['data']));
}

if (post.status === 'published') {
// When creating a new post that is published right now, signal the change
post.statusChanged = true;
}
return {posts: [post]};
});
}, function () {
return Promise.reject(new errors.NoPermissionError('You do not have permission to add posts.'));
// Push all of our tasks into a `tasks` array in the correct order
tasks = [utils.validate(docName), handlePermissions, utils.convertOptions(allowedIncludes), modelQuery];

// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, object, options).then(function formatResponse(result) {
var post = result.toJSON(options);

if (post.status === 'published') {
// When creating a new post that is published right now, signal the change
post.statusChanged = true;
}
return {posts: [post]};
});
},

/**
* ### Destroy
* ## Destroy
* Delete a post, cleans up tag relations, but not unused tags
*
* @public
* @param {{id (required), context,...}} options
* @return {Promise(Post)} Deleted Post
*/
destroy: function destroy(options) {
return canThis(options.context).destroy.post(options.id).then(function () {
var readOptions = _.extend({}, options, {status: 'all'});
return posts.read(readOptions).then(function (result) {
return dataProvider.Post.destroy(options).then(function () {
var deletedObj = result;
var tasks;

if (deletedObj.posts) {
_.each(deletedObj.posts, function (post) {
post.statusChanged = true;
});
}
/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).destroy.post(options.id).then(function permissionGranted() {
options.status = 'all';
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error, 'You do not have permission to remove posts.');
});
}

return deletedObj;
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function modelQuery(options) {
return posts.read(options).then(function (result) {
return dataProvider.Post.destroy(options).then(function () {
return result;
});
});
}, function () {
return Promise.reject(new errors.NoPermissionError('You do not have permission to remove posts.'));
}

// Push all of our tasks into a `tasks` array in the correct order
tasks = [utils.validate(docName), handlePermissions, utils.convertOptions(allowedIncludes), modelQuery];

// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options).then(function formatResponse(result) {
var deletedObj = result;

if (deletedObj.posts) {
_.each(deletedObj.posts, function (post) {
post.statusChanged = true;
});
}

return deletedObj;
});
}

};

module.exports = posts;
Loading

0 comments on commit 51ac3f6

Please sign in to comment.