// # Posts API // RESTful API for the Post resource var Promise = require('bluebird'), _ = require('lodash'), pipeline = require('../utils/pipeline'), apiUtils = require('./utils'), models = require('../models'), errors = require('../errors'), i18n = require('../i18n'), docName = 'posts', allowedIncludes = [ 'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields' ], unsafeAttrs = ['author_id'], posts; /** * ### Posts API Methods * * **See:** [API Methods](index.js.html#api%20methods) */ posts = { /** * ## 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 * * * @public * @param {{context, page, limit, status, staticPages, tag, featured}} options (optional) * @returns {Promise} Posts Collection with Meta */ browse: function browse(options) { var extraOptions = ['status', 'formats'], permittedOptions, tasks; // Workaround to remove static pages from results // TODO: rework after https://github.com/TryGhost/Ghost/issues/5151 if (options && options.context && (options.context.user || options.context.internal)) { extraOptions.push('staticPages'); } permittedOptions = apiUtils.browseDefaultOptions.concat(extraOptions); /** * ### Model Query * Make the call to the Model layer * @param {Object} options * @returns {Object} options */ function modelQuery(options) { return models.Post.findPage(options); } // Push all of our tasks into a `tasks` array in the correct order tasks = [ apiUtils.validate(docName, {opts: permittedOptions}), apiUtils.handlePublicPermissions(docName, 'browse', unsafeAttrs), apiUtils.convertOptions(allowedIncludes, models.Post.allowedFormats), modelQuery ]; // Pipeline calls each task passing the result of one to be the arguments for the next return pipeline(tasks, options); }, /** * ## Read * Find a post, by ID, UUID, or Slug * * @public * @param {Object} options * @return {Promise} Post */ read: function read(options) { var attrs = ['id', 'slug', 'status', 'uuid'], // NOTE: the scheduler API uses the post API and forwards custom options extraAllowedOptions = options.opts || ['formats'], tasks; /** * ### Model Query * Make the call to the Model layer * @param {Object} options * @returns {Object} options */ function modelQuery(options) { return models.Post.findOne(options.data, _.omit(options, ['data'])) .then(function onModelResponse(model) { if (!model) { return Promise.reject(new errors.NotFoundError({ message: i18n.t('errors.api.posts.postNotFound') })); } return { posts: [model.toJSON(options)] }; }); } // Push all of our tasks into a `tasks` array in the correct order tasks = [ apiUtils.validate(docName, {attrs: attrs, opts: extraAllowedOptions}), apiUtils.handlePublicPermissions(docName, 'read', unsafeAttrs), apiUtils.convertOptions(allowedIncludes, models.Post.allowedFormats), modelQuery ]; // Pipeline calls each task passing the result of one to be the arguments for the next return pipeline(tasks, options); }, /** * ## Edit * Update properties of a post * * @public * @param {Post} object Post or specific properties to update * @param {{id (required), context, include,...}} options * @return {Promise(Post)} Edited Post */ edit: function edit(object, options) { var tasks, // NOTE: the scheduler API uses the post API and forwards custom options extraAllowedOptions = options.opts || []; /** * ### Model Query * Make the call to the Model layer * @param {Object} options * @returns {Object} options */ function modelQuery(options) { return models.Post.edit(options.data.posts[0], _.omit(options, ['data'])) .then(function onModelResponse(model) { if (!model) { return Promise.reject(new errors.NotFoundError({ message: i18n.t('errors.api.posts.postNotFound') })); } var post = model.toJSON(options); // If previously was not published and now is (or vice versa), signal the change // @TODO: `statusChanged` get's added for the API headers only. Reconsider this. post.statusChanged = false; if (model.updated('status') !== model.get('status')) { post.statusChanged = true; } return { posts: [post] }; }); } // Push all of our tasks into a `tasks` array in the correct order tasks = [ apiUtils.validate(docName, {opts: apiUtils.idDefaultOptions.concat(extraAllowedOptions)}), apiUtils.handlePermissions(docName, 'edit', unsafeAttrs), apiUtils.convertOptions(allowedIncludes), modelQuery ]; // Pipeline calls each task passing the result of one to be the arguments for the next return pipeline(tasks, object, options); }, /** * ## Add * Create a new post along with any tags * * @public * @param {Post} object * @param {{context, include,...}} options * @return {Promise(Post)} Created Post */ add: function add(object, options) { var tasks; /** * ### Model Query * Make the call to the Model layer * @param {Object} options * @returns {Object} options */ function modelQuery(options) { return models.Post.add(options.data.posts[0], _.omit(options, ['data'])) .then(function onModelResponse(model) { var post = model.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]}; }); } // Push all of our tasks into a `tasks` array in the correct order tasks = [ apiUtils.validate(docName), apiUtils.handlePermissions(docName, 'add', unsafeAttrs), apiUtils.convertOptions(allowedIncludes), modelQuery ]; // Pipeline calls each task passing the result of one to be the arguments for the next return pipeline(tasks, object, options); }, /** * ## Destroy * Delete a post, cleans up tag relations, but not unused tags * * @public * @param {{id (required), context,...}} options * @return {Promise} */ destroy: function destroy(options) { var tasks; /** * @function deletePost * @param {Object} options */ function deletePost(options) { var Post = models.Post, data = _.defaults({status: 'all'}, options), fetchOpts = _.defaults({require: true, columns: 'id'}, options); return Post.findOne(data, fetchOpts).then(function () { return Post.destroy(options).return(null); }).catch(Post.NotFoundError, function () { throw new errors.NotFoundError({message: i18n.t('errors.api.posts.postNotFound')}); }); } // Push all of our tasks into a `tasks` array in the correct order tasks = [ apiUtils.validate(docName, {opts: apiUtils.idDefaultOptions}), apiUtils.handlePermissions(docName, 'destroy', unsafeAttrs), apiUtils.convertOptions(allowedIncludes), deletePost ]; // Pipeline calls each task passing the result of one to be the arguments for the next return pipeline(tasks, options); } }; module.exports = posts;