// # Ghost Data API // Provides access to the data model var Ghost = require('../../ghost'), _ = require('underscore'), when = require('when'), errors = require('../errorHandling'), permissions = require('../permissions'), db = require('./db'), canThis = permissions.canThis, ghost = new Ghost(), dataProvider = ghost.dataProvider, posts, users, tags, notifications, settings, requestHandler, settingsObject, settingsCollection, settingsFilter, filteredUserAttributes = ['password', 'created_by', 'updated_by', 'last_login'], ONE_DAY = 86400000; // ## Posts posts = { // #### Browse // **takes:** filter / pagination parameters browse: function browse(options) { // **returns:** a promise for a page of posts in a json object //return dataProvider.Post.findPage(options); return dataProvider.Post.findPage(options).then(function (result) { var i = 0, omitted = result; for (i = 0; i < omitted.posts.length; i = i + 1) { omitted.posts[i].author = _.omit(omitted.posts[i].author, filteredUserAttributes); omitted.posts[i].user = _.omit(omitted.posts[i].user, filteredUserAttributes); } return omitted; }); }, // #### Read // **takes:** an identifier (id or slug?) read: function read(args) { // **returns:** a promise for a single post in a json object return dataProvider.Post.findOne(args).then(function (result) { var omitted; if (result) { omitted = result.toJSON(); omitted.author = _.omit(omitted.author, filteredUserAttributes); omitted.user = _.omit(omitted.user, filteredUserAttributes); return omitted; } return when.reject({errorCode: 404, message: 'Post not found'}); }); }, // #### Edit // **takes:** a json object with all the properties which should be updated edit: function edit(postData) { // **returns:** a promise for the resulting post in a json object if (!this.user) { return when.reject({errorCode: 403, message: 'You do not have permission to edit this post.'}); } var self = this; return canThis(self.user).edit.post(postData.id).then(function () { return dataProvider.Post.edit(postData).then(function (result) { if (result) { var omitted = result.toJSON(); omitted.author = _.omit(omitted.author, filteredUserAttributes); omitted.user = _.omit(omitted.user, filteredUserAttributes); return omitted; } return when.reject({errorCode: 404, message: 'Post not found'}); }).otherwise(function (error) { return dataProvider.Post.findOne({id: postData.id, status: 'all'}).then(function (result) { if (!result) { return when.reject({errorCode: 404, message: 'Post not found'}); } return when.reject({message: error.message}); }); }); }, function () { return when.reject({errorCode: 403, message: 'You do not have permission to edit this post.'}); }); }, // #### Add // **takes:** a json object representing a post, add: function add(postData) { // **returns:** a promise for the resulting post in a json object if (!this.user) { return when.reject({errorCode: 403, message: 'You do not have permission to add posts.'}); } return canThis(this.user).create.post().then(function () { return dataProvider.Post.add(postData); }, function () { return when.reject({errorCode: 403, message: 'You do not have permission to add posts.'}); }); }, // #### Destroy // **takes:** an identifier (id or slug?) destroy: function destroy(args) { // **returns:** a promise for a json response with the id of the deleted post if (!this.user) { return when.reject({errorCode: 403, message: 'You do not have permission to remove posts.'}); } return canThis(this.user).remove.post(args.id).then(function () { return when(posts.read({id : args.id, status: 'all'})).then(function (result) { return dataProvider.Post.destroy(args.id).then(function () { var deletedObj = {}; deletedObj.id = result.id; deletedObj.slug = result.slug; return deletedObj; }); }); }, function () { return when.reject({errorCode: 403, message: 'You do not have permission to remove posts.'}); }); } }; // ## Users users = { // #### Browse // **takes:** options object browse: function browse(options) { // **returns:** a promise for a collection of users in a json object return dataProvider.User.browse(options).then(function (result) { var i = 0, omitted = {}; if (result) { omitted = result.toJSON(); } for (i = 0; i < omitted.length; i = i + 1) { omitted[i] = _.omit(omitted[i], filteredUserAttributes); } return omitted; }); }, // #### Read // **takes:** an identifier (id or slug?) read: function read(args) { // **returns:** a promise for a single user in a json object if (args.id === 'me') { args = {id: this.user}; } return dataProvider.User.read(args).then(function (result) { if (result) { var omitted = _.omit(result.toJSON(), filteredUserAttributes); return omitted; } return when.reject({errorCode: 404, message: 'User not found'}); }); }, // #### Edit // **takes:** a json object representing a user edit: function edit(userData) { // **returns:** a promise for the resulting user in a json object userData.id = this.user; return dataProvider.User.edit(userData).then(function (result) { if (result) { var omitted = _.omit(result.toJSON(), filteredUserAttributes); return omitted; } return when.reject({errorCode: 404, message: 'User not found'}); }); }, // #### Add // **takes:** a json object representing a user add: function add(userData) { // **returns:** a promise for the resulting user in a json object return dataProvider.User.add(userData); }, // #### Check // Checks a password matches the given email address // **takes:** a json object representing a user check: function check(userData) { // **returns:** on success, returns a promise for the resulting user in a json object return dataProvider.User.check(userData); }, // #### Change Password // **takes:** a json object representing a user changePassword: function changePassword(userData) { // **returns:** on success, returns a promise for the resulting user in a json object return dataProvider.User.changePassword(userData); }, generateResetToken: function generateResetToken(email) { // TODO: Do we want to be able to pass this in? var expires = Date.now() + ONE_DAY; return dataProvider.User.generateResetToken(email, expires, ghost.dbHash); }, validateToken: function validateToken(token) { return dataProvider.User.validateToken(token, ghost.dbHash); }, resetPassword: function resetPassword(token, newPassword, ne2Password) { return dataProvider.User.resetPassword(token, newPassword, ne2Password, ghost.dbHash); } }; tags = { // #### All // **takes:** Nothing yet all: function browse() { // **returns:** a promise for all tags which have previously been used in a json object return dataProvider.Tag.findAll(); } }; // ## Notifications notifications = { // #### Destroy // **takes:** an identifier (id) destroy: function destroy(i) { ghost.notifications = _.reject(ghost.notifications, function (element) { return element.id === i.id; }); // **returns:** a promise for remaining notifications as a json object return when(ghost.notifications); }, // #### Add // **takes:** a notification object of the form // ``` // msg = { // type: 'error', // this can be 'error', 'success', 'warn' and 'info' // message: 'This is an error', // A string. Should fit in one line. // status: 'persistent', // or 'passive' // id: 'auniqueid' // A unique ID // }; // ``` add: function add(notification) { // **returns:** a promise for all notifications as a json object return when(ghost.notifications.push(notification)); } }; // ## Settings // ### Helpers // Turn a settings collection into a single object/hashmap settingsObject = function (settings) { if (_.isObject(settings)) { return _.reduce(settings, function (res, item, key) { if (_.isArray(item)) { res[key] = item; } else { res[key] = item.value; } return res; }, {}); } return (settings.toJSON ? settings.toJSON() : settings).reduce(function (res, item) { if (item.toJSON) { item = item.toJSON(); } if (item.key) { res[item.key] = item.value; } return res; }, {}); }; // Turn an object into a collection settingsCollection = function (settings) { return _.map(settings, function (value, key) { return { key: key, value: value }; }); }; // Filters an object based on a given filter object settingsFilter = function (settings, filter) { return _.object(_.filter(_.pairs(settings), function (setting) { if (filter) { return _.some(filter.split(','), function (f) { return setting[1].type === f; }); } return true; })); }; settings = { // #### Browse // **takes:** options object browse: function browse(options) { // **returns:** a promise for a settings json object if (ghost.settings()) { return when(ghost.settings()).then(function (settings) { //TODO: omit where type==core return settingsObject(settingsFilter(settings, options.type)); }, errors.logAndThrowError); } }, // #### Read // **takes:** either a json object containing a key, or a single key string read: function read(options) { if (_.isString(options)) { options = { key: options }; } if (ghost.settings()) { return when(ghost.settings()[options.key]).then(function (setting) { if (!setting) { return when.reject({errorCode: 404, message: 'Unable to find setting: ' + options.key}); } var res = {}; res.key = options.key; res.value = setting.value; return res; }, errors.logAndThrowError); } }, // #### Edit // **takes:** either a json object representing a collection of settings, or a key and value pair edit: function edit(key, value) { // Check for passing a collection of settings first if (_.isObject(key)) { //clean data var type = key.type; delete key.type; delete key.availableThemes; key = settingsCollection(key); return dataProvider.Settings.edit(key).then(function (result) { result.models = result; return when(ghost.readSettingsResult(result)).then(function (settings) { ghost.updateSettingsCache(settings); return settingsObject(settingsFilter(ghost.settings(), type)); }); }).otherwise(function (error) { return dataProvider.Settings.read(key.key).then(function (result) { if (!result) { return when.reject({errorCode: 404, message: 'Unable to find setting: ' + key}); } return when.reject({message: error.message}); }); }); } return dataProvider.Settings.read(key).then(function (setting) { if (!setting) { return when.reject({errorCode: 404, message: 'Unable to find setting: ' + key}); } if (!_.isString(value)) { value = JSON.stringify(value); } setting.set('value', value); return dataProvider.Settings.edit(setting).then(function (result) { ghost.settings()[_.first(result).attributes.key].value = _.first(result).attributes.value; return settingsObject(ghost.settings()); }, errors.logAndThrowError); }); } }; // ## Request Handlers function invalidateCache(req, res, result) { var parsedUrl = req._parsedUrl.pathname.replace(/\/$/, '').split('/'), method = req.method, endpoint = parsedUrl[4], id = parsedUrl[5], cacheInvalidate, jsonResult = result.toJSON ? result.toJSON() : result; if (method === 'POST' || method === 'PUT' || method === 'DELETE') { if (endpoint === 'settings' || endpoint === 'users') { cacheInvalidate = "/*"; } else if (endpoint === 'posts') { cacheInvalidate = "/, /page/*, /rss/, /rss/*"; if (id && jsonResult.slug) { cacheInvalidate += ', /' + jsonResult.slug + '/'; } } if (cacheInvalidate) { res.set({ "X-Cache-Invalidate": cacheInvalidate }); } } } // ### requestHandler // decorator for api functions which are called via an HTTP request // takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response requestHandler = function (apiMethod) { return function (req, res) { var options = _.extend(req.body, req.query, req.params), apiContext = { user: req.session && req.session.user }; return apiMethod.call(apiContext, options).then(function (result) { invalidateCache(req, res, result); res.json(result || {}); }, function (error) { var errorCode = error.errorCode || 500, errorMsg = {error: _.isString(error) ? error : (_.isObject(error) ? error.message : 'Unknown API Error')}; res.json(errorCode, errorMsg); }); }; }; // Public API module.exports.posts = posts; module.exports.users = users; module.exports.tags = tags; module.exports.notifications = notifications; module.exports.settings = settings; module.exports.db = db; module.exports.requestHandler = requestHandler;