2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Refactor API arguments

closes #2610, refs #2697

- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
  everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
  to perform reads, updates and deletes where possible - settings / themes
  may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
This commit is contained in:
Hannah Wolfe 2014-05-08 13:41:19 +01:00
parent 4378495e8e
commit c02ebb0dcf
52 changed files with 1522 additions and 1185 deletions

View file

@ -15,11 +15,11 @@ api.notifications = require('./notifications');
api.settings = require('./settings');
db = {
'exportContent': function () {
var self = this;
'exportContent': function (options) {
options = options || {};
// Export data, otherwise send error 500
return canThis(self.user).exportContent.db().then(function () {
return canThis(options.context).exportContent.db().then(function () {
return dataExport().then(function (exportedData) {
return when.resolve({ db: [exportedData] });
}).otherwise(function (error) {
@ -30,10 +30,10 @@ db = {
});
},
'importContent': function (options) {
var databaseVersion,
self = this;
options = options || {};
var databaseVersion;
return canThis(self.user).importContent.db().then(function () {
return canThis(options.context).importContent.db().then(function () {
if (!options.importfile || !options.importfile.path || options.importfile.name.indexOf('json') === -1) {
/**
* Notify of an error if it occurs
@ -46,7 +46,7 @@ db = {
return when.reject(new errors.InternalServerError('Please select a .json file to import.'));
}
return api.settings.read.call({ internal: true }, { key: 'databaseVersion' }).then(function (response) {
return api.settings.read({key: 'databaseVersion', context: { internal: true }}).then(function (response) {
var setting = response.settings[0];
return when(setting.value);
@ -108,10 +108,10 @@ db = {
return when.reject(new errors.NoPermissionError('You do not have permission to export data. (no rights)'));
});
},
'deleteAllContent': function () {
var self = this;
'deleteAllContent': function (options) {
options = options || {};
return canThis(self.user).deleteAllContent.db().then(function () {
return canThis(options.context).deleteAllContent.db().then(function () {
return when(dataProvider.deleteAllContent())
.then(function () {
return when.resolve({ db: [] });

View file

@ -4,20 +4,46 @@
var _ = require('lodash'),
when = require('when'),
config = require('../config'),
// Include Endpoints
db = require('./db'),
settings = require('./settings'),
mail = require('./mail'),
notifications = require('./notifications'),
posts = require('./posts'),
users = require('./users'),
settings = require('./settings'),
tags = require('./tags'),
themes = require('./themes'),
mail = require('./mail'),
requestHandler,
init;
users = require('./users'),
// ## Request Handlers
http,
formatHttpErrors,
cacheInvalidationHeader,
locationHeader,
contentDispositionHeader,
init,
function cacheInvalidationHeader(req, result) {
/**
* ### Init
* Initialise the API - populate the settings cache
* @return {Promise(Settings)} Resolves to Settings Collection
*/
init = function () {
return settings.updateSettingsCache();
};
/**
* ### Cache Invalidation Header
* Calculate the header string for the X-Cache-Invalidate: header.
* The resulting string instructs any cache in front of the blog that request has occurred which invalidates any cached
* versions of the listed URIs.
*
* `/*` is used to mean the entire cache is invalid
*
* @private
* @param {Express.request} req Original HTTP Request
* @param {Object} result API method result
* @return {Promise(String)} Resolves to header string
*/
cacheInvalidationHeader = function (req, result) {
var parsedUrl = req._parsedUrl.pathname.replace(/\/$/, '').split('/'),
method = req.method,
endpoint = parsedUrl[4],
@ -54,15 +80,20 @@ function cacheInvalidationHeader(req, result) {
}
return when(cacheInvalidate);
}
};
// if api request results in the creation of a new object, construct
// a Location: header that points to the new resource.
//
// arguments: request object, result object from the api call
// returns: a promise that will be fulfilled with the location of the
// resource
function locationHeader(req, result) {
/**
* ### Location Header
*
* If the API request results in the creation of a new object, construct a Location: header which points to the new
* resource.
*
* @private
* @param {Express.request} req Original HTTP Request
* @param {Object} result API method result
* @return {Promise(String)} Resolves to header string
*/
locationHeader = function (req, result) {
var apiRoot = config.urlFor('api'),
location,
post,
@ -81,98 +112,137 @@ function locationHeader(req, result) {
}
return when(location);
}
};
// create a header that invokes the 'Save As' dialog
// in the browser when exporting the database to file.
// The 'filename' parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3).
//
// for encoding whitespace and non-ISO-8859-1 characters, you MUST
// use the "filename*=" attribute, NOT "filename=". Ideally, both.
// see: http://tools.ietf.org/html/rfc598
// examples: http://tools.ietf.org/html/rfc6266#section-5
//
// we'll use ISO-8859-1 characters here to keep it simple.
function dbExportSaveAsHeader() {
/**
* ### Content Disposition Header
* create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename'
* parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3).
*
* For encoding whitespace and non-ISO-8859-1 characters, you MUST use the "filename*=" attribute, NOT "filename=".
* Ideally, both. Examples: http://tools.ietf.org/html/rfc6266#section-5
*
* We'll use ISO-8859-1 characters here to keep it simple.
*
* @private
* @see http://tools.ietf.org/html/rfc598
* @return {string}
*/
contentDispositionHeader = function () {
// replace ':' with '_' for OS that don't support it
var now = (new Date()).toJSON().replace(/:/g, '_');
return 'Attachment; filename="ghost-' + now + '.json"';
}
};
// ### 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) {
/**
* ### Format HTTP Errors
* Converts the error response from the API into a format which can be returned over HTTP
*
* @private
* @param {Array} error
* @return {{errors: Array, statusCode: number}}
*/
formatHttpErrors = function (error) {
var statusCode = 500,
errors = [];
if (!_.isArray(error)) {
error = [].concat(error);
}
_.each(error, function (errorItem) {
var errorContent = {};
//TODO: add logic to set the correct status code
statusCode = errorItem.code || 500;
errorContent.message = _.isString(errorItem) ? errorItem :
(_.isObject(errorItem) ? errorItem.message : 'Unknown API Error');
errorContent.type = errorItem.type || 'InternalServerError';
errors.push(errorContent);
});
return {errors: errors, statusCode: statusCode};
};
/**
* ### HTTP
*
* 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.
*
* @public
* @param {Function} apiMethod API method to call
* @return {Function} middleware format function to be called by the route when a matching request is made
*/
http = function (apiMethod) {
return function (req, res) {
var options = _.extend(req.body, req.files, req.query, req.params),
apiContext = {
user: (req.session && req.session.user) ? req.session.user : null
};
return apiMethod.call(apiContext, options).then(function (result) {
return cacheInvalidationHeader(req, result).then(function (header) {
if (header) {
res.set({
"X-Cache-Invalidate": header
});
// We define 2 properties for using as arguments in API calls:
var object = req.body,
options = _.extend({}, req.files, req.query, req.params, {
context: {
user: (req.session && req.session.user) ? req.session.user : null
}
})
.then(function () {
if (apiMethod === db.exportContent) {
res.set({
"Content-Disposition": dbExportSaveAsHeader()
});
}
})
.then(function () {
return locationHeader(req, result).then(function (header) {
if (header) {
res.set({
'Location': header
});
}
res.json(result || {});
});
});
}, function (error) {
var errorCode,
errors = [];
if (!_.isArray(error)) {
error = [].concat(error);
}
_.each(error, function (errorItem) {
var errorContent = {};
//TODO: add logic to set the correct status code
errorCode = errorItem.code || 500;
errorContent['message'] = _.isString(errorItem) ? errorItem : (_.isObject(errorItem) ? errorItem.message : 'Unknown API Error');
errorContent['type'] = errorItem.type || 'InternalServerError';
errors.push(errorContent);
});
res.json(errorCode, {errors: errors});
});
// If this is a GET, or a DELETE, req.body should be null, so we only have options (route and query params)
// If this is a PUT, POST, or PATCH, req.body is an object
if (_.isEmpty(object)) {
object = options;
options = {};
}
return apiMethod(object, options)
// Handle adding headers
.then(function onSuccess(result) {
// Add X-Cache-Invalidate header
return cacheInvalidationHeader(req, result)
.then(function addCacheHeader(header) {
if (header) {
res.set({'X-Cache-Invalidate': header});
}
// Add Location header
return locationHeader(req, result);
}).then(function addLocationHeader(header) {
if (header) {
res.set({'Location': header});
}
// Add Content-Disposition Header
if (apiMethod === db.exportContent) {
res.set({
'Content-Disposition': contentDispositionHeader()
});
}
// #### Success
// Send a properly formatting HTTP response containing the data with correct headers
res.json(result || {});
});
}).catch(function onError(error) {
// #### Error
var httpErrors = formatHttpErrors(error);
// Send a properly formatted HTTP response containing the errors
res.json(httpErrors.statusCode, {errors: httpErrors.errors});
});
};
};
init = function () {
return settings.updateSettingsCache();
};
// Public API
/**
* ## Public API
*/
module.exports = {
posts: posts,
users: users,
tags: tags,
themes: themes,
notifications: notifications,
settings: settings,
// Extras
init: init,
http: http,
// API Endpoints
db: db,
mail: mail,
requestHandler: requestHandler,
init: init
notifications: notifications,
posts: posts,
settings: settings,
tags: tags,
themes: themes,
users: users
};

View file

@ -4,7 +4,7 @@ var when = require('when'),
// Holds the persistent notifications
notificationsStore = [],
// Holds the last used id
// Holds the last used id
notificationCounter = 0,
notifications;
@ -15,16 +15,15 @@ notifications = {
return when({ 'notifications': notificationsStore });
},
// #### Destroy
// **takes:** an identifier object ({id: id})
destroy: function destroy(i) {
destroy: function destroy(options) {
var notification = _.find(notificationsStore, function (element) {
return element.id === parseInt(i.id, 10);
return element.id === parseInt(options.id, 10);
});
if (notification && !notification.dismissable) {
return when.reject(new errors.NoPermissionError('You do not have permission to dismiss this notification.'));
return when.reject(
new errors.NoPermissionError('You do not have permission to dismiss this notification.')
);
}
if (!notification) {
@ -32,7 +31,7 @@ notifications = {
}
notificationsStore = _.reject(notificationsStore, function (element) {
return element.id === parseInt(i.id, 10);
return element.id === parseInt(options.id, 10);
});
// **returns:** a promise for the deleted object
return when({notifications: [notification]});
@ -44,17 +43,20 @@ notifications = {
return when(notificationsStore);
},
// #### 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.
// location: 'bottom', // A string where this notification should appear. can be 'bottom' or 'top'
// dismissable: true // A Boolean. Whether the notification is dismissable or not.
// };
// ```
/**
* ### 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.
* location: 'bottom', // A string where this notification should appear. can be 'bottom' or 'top'
* dismissable: true // A Boolean. Whether the notification is dismissable or not.
* };
* ```
*/
add: function add(notification) {
var defaults = {
@ -64,7 +66,7 @@ notifications = {
};
notificationCounter = notificationCounter + 1;
notification = _.assign(defaults, notification, {
id: notificationCounter
//status: 'persistent'

View file

@ -1,23 +1,20 @@
var when = require('when'),
_ = require('lodash'),
dataProvider = require('../models'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
// # Posts API
var when = require('when'),
_ = require('lodash'),
dataProvider = require('../models'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
utils = require('./utils'),
allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields'],
docName = 'posts',
allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields'],
posts;
function checkPostData(postData) {
if (_.isEmpty(postData) || _.isEmpty(postData.posts) || _.isEmpty(postData.posts[0])) {
return when.reject(new errors.BadRequestError('No root key (\'posts\') provided.'));
}
return when.resolve(postData);
}
// ## Helpers
function prepareInclude(include) {
var index;
include = _.intersection(include.split(","), allowedIncludes);
include = _.intersection(include.split(','), allowedIncludes);
index = include.indexOf('author');
if (index !== -1) {
@ -27,16 +24,20 @@ function prepareInclude(include) {
return include;
}
// ## Posts
// ## API Methods
posts = {
// #### Browse
// **takes:** filter / pagination parameters
/**
* ### Browse
* Find a paginated set of posts
* @param {{context, page, limit, status, staticPages, tag}} options (optional)
* @returns {Promise(Posts)} Posts Collection with Meta
*/
browse: function browse(options) {
options = options || {};
// only published posts if no user is present
if (!this.user) {
if (!(options.context && options.context.user)) {
options.status = 'published';
}
@ -44,28 +45,30 @@ posts = {
options.include = prepareInclude(options.include);
}
// **returns:** a promise for a page of posts in a json object
return dataProvider.Post.findPage(options);
},
// #### Read
// **takes:** an identifier (id or slug?)
/**
* ### Read
* Find a post, by ID or Slug
* @param {{id_or_slug (required), context, status, include, ...}} options
* @return {Promise(Post)} Post
*/
read: function read(options) {
var include;
options = options || {};
var attrs = ['id', 'slug', 'status'],
data = _.pick(options, attrs);
options = _.omit(options, attrs);
// only published posts if no user is present
if (!this.user) {
options.status = 'published';
if (!(options.context && options.context.user)) {
data.status = 'published';
}
if (options.include) {
include = prepareInclude(options.include);
delete options.include;
options.include = prepareInclude(options.include);
}
// **returns:** a promise for a single post in a json object
return dataProvider.Post.findOne(options, {include: include}).then(function (result) {
return dataProvider.Post.findOne(data, options).then(function (result) {
if (result) {
return { posts: [ result.toJSON() ]};
}
@ -75,21 +78,21 @@ posts = {
});
},
// #### 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
var self = this,
include;
return canThis(this).edit.post(postData.id).then(function () {
return checkPostData(postData).then(function (checkedPostData) {
if (postData.include) {
include = prepareInclude(postData.include);
/**
* ### Edit
* Update properties of a post
* @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) {
return canThis(options.context).edit.post(options.id).then(function () {
return utils.checkObject(object, docName).then(function (checkedPostData) {
if (options.include) {
options.include = prepareInclude(options.include);
}
return dataProvider.Post.edit(checkedPostData.posts[0], {user: self.user, include: include});
return dataProvider.Post.edit(checkedPostData.posts[0], options);
}).then(function (result) {
if (result) {
var post = result.toJSON();
@ -108,20 +111,23 @@ posts = {
});
},
// #### Add
// **takes:** a json object representing a post,
add: function add(postData) {
var self = this,
include;
/**
* ### Add
* Create a new post along with any tags
* @param {Post} object
* @param {{context, include,...}} options
* @return {Promise(Post)} Created Post
*/
add: function add(object, options) {
options = options || {};
// **returns:** a promise for the resulting post in a json object
return canThis(this).create.post().then(function () {
return checkPostData(postData).then(function (checkedPostData) {
if (postData.include) {
include = prepareInclude(postData.include);
return canThis(options.context).create.post().then(function () {
return utils.checkObject(object, docName).then(function (checkedPostData) {
if (options.include) {
options.include = prepareInclude(options.include);
}
return dataProvider.Post.add(checkedPostData.posts[0], {user: self.user, include: include});
return dataProvider.Post.add(checkedPostData.posts[0], options);
}).then(function (result) {
var post = result.toJSON();
@ -136,15 +142,18 @@ posts = {
});
},
// #### Destroy
// **takes:** an identifier (id or slug?)
destroy: function destroy(args) {
var self = this;
// **returns:** a promise for a json response with the id of the deleted post
return canThis(this).remove.post(args.id).then(function () {
// TODO: Would it be good to get rid of .call()?
return posts.read.call({user: self.user}, {id : args.id, status: 'all'}).then(function (result) {
return dataProvider.Post.destroy(args.id).then(function () {
/**
* ### Destroy
* Delete a post, cleans up tag relations, but not unused tags
* @param {{id (required), context,...}} options
* @return {Promise(Post)} Deleted Post
*/
destroy: function destroy(options) {
return canThis(options.context).remove.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;
if (deletedObj.posts) {
@ -161,17 +170,21 @@ posts = {
});
},
// #### Generate slug
// **takes:** a string to generate the slug from
generateSlug: function generateSlug(args) {
return canThis(this).slug.post().then(function () {
return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) {
if (slug) {
return slug;
}
return when.reject(new errors.InternalServerError('Could not generate slug'));
});
/**
* ## Generate Slug
* Create a unique slug for a given post title
* @param {{title (required), transacting}} options
* @returns {Promise(String)} Unique string
*/
generateSlug: function generateSlug(options) {
return canThis(options.context).slug.post().then(function () {
return dataProvider.Base.Model.generateSlug(dataProvider.Post, options.title, {status: 'all'})
.then(function (slug) {
if (slug) {
return slug;
}
return when.reject(new errors.InternalServerError('Could not generate slug'));
});
}, function () {
return when.reject(new errors.NoPermissionError('You do not have permission.'));
});

View file

@ -1,33 +1,38 @@
// # Settings API
var _ = require('lodash'),
dataProvider = require('../models'),
when = require('when'),
config = require('../config'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
utils = require('./utils'),
docName = 'settings',
settings,
settingsFilter,
updateSettingsCache,
readSettingsResult,
settingsFilter,
filterPaths,
readSettingsResult,
settingsResult,
// Holds cached settings
canEditAllSettings,
/**
* ## Cache
* Holds cached settings
* @private
* @type {{}}
*/
settingsCache = {};
// ### Helpers
// 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;
}));
};
// Maintain the internal cache of the settings object
/**
* ### Update Settings Cache
* Maintain the internal cache of the settings object
* @public
* @param settings
* @returns {Settings}
*/
updateSettingsCache = function (settings) {
settings = settings || {};
@ -47,6 +52,78 @@ updateSettingsCache = function (settings) {
});
};
// ## Helpers
/**
* ### Settings Filter
* Filters an object based on a given filter object
* @private
* @param settings
* @param filter
* @returns {*}
*/
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;
}));
};
/**
* ### Filter Paths
* Normalizes paths read by require-tree so that the apps and themes modules can use them. Creates an empty
* array (res), and populates it with useful info about the read packages like name, whether they're active
* (comparison with the second argument), and if they have a package.json, that, otherwise false
* @private
* @param {object} paths as returned by require-tree()
* @param {array/string} active as read from the settings object
* @returns {Array} of objects with useful info about apps / themes
*/
filterPaths = function (paths, active) {
var pathKeys = Object.keys(paths),
res = [],
item;
// turn active into an array (so themes and apps can be checked the same)
if (!Array.isArray(active)) {
active = [active];
}
_.each(pathKeys, function (key) {
//do not include hidden files or _messages
if (key.indexOf('.') !== 0 &&
key !== '_messages' &&
key !== 'README.md'
) {
item = {
name: key
};
if (paths[key].hasOwnProperty('package.json')) {
item.package = paths[key]['package.json'];
} else {
item.package = false;
}
if (_.indexOf(active, key) !== -1) {
item.active = true;
}
res.push(item);
}
});
return res;
};
/**
* ### Read Settings Result
* @private
* @param settingsModels
* @returns {Settings}
*/
readSettingsResult = function (settingsModels) {
var settings = _.reduce(settingsModels, function (memo, member) {
if (!memo.hasOwnProperty(member.attributes.key)) {
@ -82,79 +159,78 @@ readSettingsResult = function (settingsModels) {
return settings;
};
// Normalizes paths read by require-tree so that the apps and themes modules can use them.
// Creates an empty array (res), and populates it with useful info about the read packages
// like name, whether they're active (comparison with the second argument), and if they
// have a package.json, that, otherwise false
// @param {object} paths as returned by require-tree()
// @param {array/string} active as read from the settings object
// @return {array} of objects with useful info about apps / themes
filterPaths = function (paths, active) {
var pathKeys = Object.keys(paths),
res = [],
item;
// turn active into an array (so themes and apps can be checked the same)
if (!Array.isArray(active)) {
active = [active];
}
_.each(pathKeys, function (key) {
//do not include hidden files or _messages
if (key.indexOf('.') !== 0 &&
key !== '_messages' &&
key !== 'README.md'
) {
item = {
name: key
};
if (paths[key].hasOwnProperty('package.json')) {
item.package = paths[key]['package.json'];
} else {
item.package = false;
}
if (_.indexOf(active, key) !== -1) {
item.active = true;
}
res.push(item);
}
});
return res;
};
/**
* ### Settings Result
* @private
* @param settings
* @param type
* @returns {{settings: *}}
*/
settingsResult = function (settings, type) {
var filteredSettings = _.values(settingsFilter(settings, type)),
result = {
settings: filteredSettings
settings: filteredSettings,
meta: {}
};
if (type) {
result.meta = {
filters: {
type: type
}
result.meta.filters = {
type: type
};
}
return result;
};
/**
* ### Can Edit All Settings
* Check that this edit request is allowed for all settings requested to be updated
* @private
* @param settingsInfo
* @returns {*}
*/
canEditAllSettings = function (settingsInfo, options) {
var checks = _.map(settingsInfo, function (settingInfo) {
var setting = settingsCache[settingInfo.key];
if (!setting) {
return when.reject(new errors.NotFoundError('Unable to find setting: ' + settingInfo.key));
}
if (setting.type === 'core' && !(options.context && options.context.internal)) {
return when.reject(
new errors.NoPermissionError('Attempted to access core setting from external request')
);
}
return canThis(options.context).edit.setting(settingInfo.key);
});
return when.all(checks);
};
// ## API Methods
settings = {
// #### Browse
// **takes:** options object
/**
* ### Browse
* @param options
* @returns {*}
*/
browse: function browse(options) {
var self = this;
options = options || {};
// **returns:** a promise for a settings json object
return canThis(this).browse.setting().then(function () {
var result = settingsResult(settingsCache, options.type);
var result = settingsResult(settingsCache, options.type);
// If there is no context, return only blog settings
if (!options.context) {
return when(_.filter(result.settings, function (setting) { return setting.type === 'blog'; }));
}
// Otherwise return whatever this context is allowed to browse
return canThis(options.context).browse.setting().then(function () {
// Omit core settings unless internal request
if (!self.internal) {
if (!options.context.internal) {
result.settings = _.filter(result.settings, function (setting) { return setting.type !== 'core'; });
}
@ -162,90 +238,88 @@ settings = {
});
},
// #### Read
// **takes:** either a json object containing a key, or a single key string
/**
* ### Read
* @param options
* @returns {*}
*/
read: function read(options) {
if (_.isString(options)) {
options = { key: options };
}
var self = this;
var setting = settingsCache[options.key],
result = {};
return canThis(this).read.setting(options.key).then(function () {
var setting = settingsCache[options.key],
result = {};
if (!setting) {
return when.reject(new errors.NotFoundError('Unable to find setting: ' + options.key));
}
if (!setting) {
return when.reject(new errors.NotFoundError('Unable to find setting: ' + options.key));
}
result[options.key] = setting;
if (!self.internal && setting.type === 'core') {
return when.reject(new errors.NoPermissionError('Attempted to access core setting on external request'));
}
if (setting.type === 'core' && !(options.context && options.context.internal)) {
return when.reject(
new errors.NoPermissionError('Attempted to access core setting from external request')
);
}
result[options.key] = setting;
if (setting.type === 'blog') {
return when(settingsResult(result));
}
return canThis(options.context).read.setting(options.key).then(function () {
return settingsResult(result);
}, function () {
return when.reject(new errors.NoPermissionError('You do not have permission to read settings.'));
});
},
// #### Edit
// **takes:** either a json object representing a collection of settings, or a key and value pair
edit: function edit(key, value) {
/**
* ### Edit
* Update properties of a post
* @param {{settings: }} object Setting or a single string name
* @param {{id (required), include,...}} options (optional) or a single string value
* @return {Promise(Setting)} Edited Setting
*/
edit: function edit(object, options) {
options = options || {};
var self = this,
type,
canEditAllSettings = function (settingsInfo) {
var checks = _.map(settingsInfo, function (settingInfo) {
var setting = settingsCache[settingInfo.key];
type;
if (!setting) {
return when.reject(new errors.NotFoundError('Unable to find setting: ' + settingInfo.key));
}
if (!self.internal && setting.type === 'core') {
return when.reject(new errors.NoPermissionError('Attempted to access core setting on external request'));
}
return canThis(self).edit.setting(settingInfo.key);
});
return when.all(checks);
};
if (!_.isString(value)) {
value = JSON.stringify(value);
}
// Allow shorthand syntax
if (_.isString(key)) {
key = { settings: [{ key: key, value: value }]};
// Allow shorthand syntax where a single key and value are passed to edit instead of object and options
if (_.isString(object)) {
object = { settings: [{ key: object, value: options }]};
}
//clean data
type = _.find(key.settings, function (setting) { return setting.key === 'type'; });
_.each(object.settings, function (setting) {
if (!_.isString(setting.value)) {
setting.value = JSON.stringify(setting.value);
}
});
type = _.find(object.settings, function (setting) { return setting.key === 'type'; });
if (_.isObject(type)) {
type = type.value;
}
key = _.reject(key.settings, function (setting) {
object.settings = _.reject(object.settings, function (setting) {
return setting.key === 'type' || setting.key === 'availableThemes' || setting.key === 'availableApps';
});
return canEditAllSettings(key).then(function () {
return dataProvider.Settings.edit(key, {user: self.user});
}).then(function (result) {
var readResult = readSettingsResult(result);
return canEditAllSettings(object.settings, options).then(function () {
return utils.checkObject(object, docName).then(function (checkedData) {
options.user = self.user;
return dataProvider.Settings.edit(checkedData.settings, options);
}).then(function (result) {
var readResult = readSettingsResult(result);
return updateSettingsCache(readResult).then(function () {
return config.theme.update(settings, config().url);
}).then(function () {
return settingsResult(readResult, type);
return updateSettingsCache(readResult).then(function () {
return config.theme.update(settings, config().url);
}).then(function () {
return settingsResult(readResult, type);
});
});
}).catch(function (error) {
// Pass along API error
return when.reject(error);
});
}
};

View file

@ -3,11 +3,8 @@ var dataProvider = require('../models'),
tags = {
// #### Browse
// **takes:** Nothing yet
browse: function browse() {
// **returns:** a promise for all tags which have previously been used in a json object
return dataProvider.Tag.findAll().then(function (result) {
browse: function browse(options) {
return dataProvider.Tag.findAll(options).then(function (result) {
return { tags: result.toJSON() };
});
}

View file

@ -10,11 +10,12 @@ var when = require('when'),
// ## Themes
themes = {
browse: function browse() {
// **returns:** a promise for a collection of themes in a json object
return canThis(this).browse.theme().then(function () {
browse: function browse(options) {
options = options || {};
return canThis(options.context).browse.theme().then(function () {
return when.all([
settings.read.call({ internal: true }, 'activeTheme'),
settings.read({key: 'activeTheme', context: {internal: true}}),
config().paths.availableThemes
]).then(function (result) {
var activeTheme = result[0].settings[0].value,
@ -49,19 +50,18 @@ themes = {
});
},
edit: function edit(themeData) {
var self = this,
themeName;
edit: function edit(object, options) {
var themeName;
// Check whether the request is properly formatted.
if (!_.isArray(themeData.themes)) {
if (!_.isArray(object.themes)) {
return when.reject({type: 'BadRequest', message: 'Invalid request.'});
}
themeName = themeData.themes[0].uuid;
themeName = object.themes[0].uuid;
return canThis(this).edit.theme().then(function () {
return themes.browse.call(self).then(function (availableThemes) {
return canThis(options.context).edit.theme().then(function () {
return themes.browse(options).then(function (availableThemes) {
var theme;
// Check if the theme exists
@ -73,8 +73,10 @@ themes = {
return when.reject(new errors.BadRequestError('Theme does not exist.'));
}
// Activate the theme
return settings.edit.call({ internal: true }, 'activeTheme', themeName).then(function () {
// Activate the theme
return settings.edit(
{settings: [{ key: 'activeTheme', value: themeName }]}, {context: {internal: true }}
).then(function () {
theme.active = true;
return { themes: [theme]};
});

View file

@ -1,27 +1,26 @@
var when = require('when'),
_ = require('lodash'),
dataProvider = require('../models'),
settings = require('./settings'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
ONE_DAY = 86400000,
var when = require('when'),
_ = require('lodash'),
dataProvider = require('../models'),
settings = require('./settings'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
utils = require('./utils'),
docName = 'users',
ONE_DAY = 86400000,
users;
function checkUserData(userData) {
if (_.isEmpty(userData) || _.isEmpty(userData.users) || _.isEmpty(userData.users[0])) {
return when.reject(new errors.BadRequestError('No root key (\'users\') provided.'));
}
return when.resolve(userData);
}
// ## Users
users = {
// #### Browse
// **takes:** options object
/**
* ## Browse
* Fetch all users
* @param {object} options (optional)
* @returns {Promise(Users)} Users Collection
*/
browse: function browse(options) {
// **returns:** a promise for a collection of users in a json object
return canThis(this).browse.user().then(function () {
options = options || {};
return canThis(options.context).browse.user().then(function () {
return dataProvider.User.findAll(options).then(function (result) {
return { users: result.toJSON() };
});
@ -30,15 +29,17 @@ users = {
});
},
// #### 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};
read: function read(options) {
var attrs = ['id'],
data = _.pick(options, attrs);
options = _.omit(options, attrs);
if (data.id === 'me' && options.context && options.context.user) {
data.id = options.context.user;
}
return dataProvider.User.findOne(args).then(function (result) {
return dataProvider.User.findOne(data, options).then(function (result) {
if (result) {
return { users: [result.toJSON()] };
}
@ -47,14 +48,15 @@ users = {
});
},
// #### Edit
// **takes:** a json object representing a user
edit: function edit(userData) {
// **returns:** a promise for the resulting user in a json object
var self = this;
return canThis(this).edit.user(userData.users[0].id).then(function () {
return checkUserData(userData).then(function (checkedUserData) {
return dataProvider.User.edit(checkedUserData.users[0], {user: self.user});
edit: function edit(object, options) {
if (options.id === 'me' && options.context && options.context.user) {
options.id = options.context.user;
}
return canThis(options.context).edit.user(options.id).then(function () {
return utils.checkObject(object, docName).then(function (checkedUserData) {
return dataProvider.User.edit(checkedUserData.users[0], options);
}).then(function (result) {
if (result) {
return { users: [result.toJSON()]};
@ -66,19 +68,17 @@ users = {
});
},
// #### Add
// **takes:** a json object representing a user
add: function add(userData) {
// **returns:** a promise for the resulting user in a json object
var self = this;
return canThis(this).add.user().then(function () {
return checkUserData(userData).then(function (checkedUserData) {
// if the user is created by users.register(), use id: 1
// as the creator for now
if (self.internal) {
self.user = 1;
add: function add(object, options) {
options = options || {};
return canThis(options.context).add.user().then(function () {
return utils.checkObject(object, docName).then(function (checkedUserData) {
// if the user is created by users.register(), use id: 1 as the creator for now
if (options.context.internal) {
options.context.user = 1;
}
return dataProvider.User.add(checkedUserData.users[0], {user: self.user});
return dataProvider.User.add(checkedUserData.users[0], options);
}).then(function (result) {
if (result) {
return { users: [result.toJSON()]};
@ -89,47 +89,37 @@ users = {
});
},
// #### Register
// **takes:** a json object representing a user
register: function register(userData) {
// TODO: if we want to prevent users from being created with the signup form
// this is the right place to do it
return users.add.call({internal: true}, userData);
register: function register(object) {
// TODO: if we want to prevent users from being created with the signup form this is the right place to do it
return users.add(object, {context: {internal: true}});
},
// #### 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);
check: function check(object) {
return dataProvider.User.check(object);
},
// #### 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);
changePassword: function changePassword(object) {
return dataProvider.User.changePassword(object);
},
generateResetToken: function generateResetToken(email) {
var expires = Date.now() + ONE_DAY;
return settings.read.call({ internal: true }, 'dbHash').then(function (response) {
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
var dbHash = response.settings[0].value;
return dataProvider.User.generateResetToken(email, expires, dbHash);
});
},
validateToken: function validateToken(token) {
return settings.read.call({ internal: true }, 'dbHash').then(function (response) {
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
var dbHash = response.settings[0].value;
return dataProvider.User.validateToken(token, dbHash);
});
},
resetPassword: function resetPassword(token, newPassword, ne2Password) {
return settings.read.call({ internal: true }, 'dbHash').then(function (response) {
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
var dbHash = response.settings[0].value;
return dataProvider.User.resetPassword(token, newPassword, ne2Password, dbHash);
});

14
core/server/api/utils.js Normal file
View file

@ -0,0 +1,14 @@
var when = require('when'),
_ = require('lodash'),
utils;
utils = {
checkObject: function (object, docName) {
if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) {
return when.reject({type: 'BadRequest', message: 'No root key (\'' + docName + '\') provided.'});
}
return when.resolve(object);
}
};
module.exports = utils;

View file

@ -9,7 +9,7 @@ var _ = require('lodash'),
function getInstalledApps() {
return api.settings.read.call({ internal: true }, 'installedApps').then(function (response) {
return api.settings.read({context: {internal: true}, key: 'installedApps'}).then(function (response) {
var installed = response.settings[0];
installed.value = installed.value || '[]';
@ -28,7 +28,7 @@ function saveInstalledApps(installedApps) {
return getInstalledApps().then(function (currentInstalledApps) {
var updatedAppsInstalled = _.uniq(installedApps.concat(currentInstalledApps));
return api.settings.edit.call({internal: true}, 'installedApps', updatedAppsInstalled);
return api.settings.edit({context: {internal: true}, key: 'installedApps'}, updatedAppsInstalled);
});
}
@ -38,7 +38,7 @@ module.exports = {
try {
// We have to parse the value because it's a string
api.settings.read.call({ internal: true }, 'activeApps').then(function (response) {
api.settings.read({context: {internal: true}, key: 'activeApps'}).then(function (response) {
var aApps = response.settings[0];
appsToLoad = JSON.parse(aApps.value) || [];

View file

@ -39,7 +39,13 @@ var generateProxyFunctions = function (name, permissions) {
return _.reduce(apiMethods, function (memo, apiMethod, methodName) {
memo[methodName] = function () {
return apiMethod.apply(_.clone(appContext), _.toArray(arguments));
var args = _.toArray(arguments),
options = args[args.length - 1];
if (_.isObject(options)) {
options.context = _.clone(appContext);
}
return apiMethod.apply({}, args);
};
return memo;
@ -57,10 +63,18 @@ var generateProxyFunctions = function (name, permissions) {
registerAsync: checkRegisterPermissions('helpers', helpers.registerAsyncThemeHelper.bind(helpers))
},
api: {
posts: passThruAppContextToApi('posts', _.pick(api.posts, 'browse', 'read', 'edit', 'add', 'destroy')),
tags: passThruAppContextToApi('tags', _.pick(api.tags, 'browse')),
notifications: passThruAppContextToApi('notifications', _.pick(api.notifications, 'browse', 'add', 'destroy')),
settings: passThruAppContextToApi('settings', _.pick(api.settings, 'browse', 'read', 'edit'))
posts: passThruAppContextToApi('posts',
_.pick(api.posts, 'browse', 'read', 'edit', 'add', 'destroy')
),
tags: passThruAppContextToApi('tags',
_.pick(api.tags, 'browse')
),
notifications: passThruAppContextToApi('notifications',
_.pick(api.notifications, 'browse', 'add', 'destroy')
),
settings: passThruAppContextToApi('settings',
_.pick(api.settings, 'browse', 'read', 'edit')
)
}
};

View file

@ -19,10 +19,10 @@ function theme() {
function update(settings, configUrl) {
// TODO: Pass the context into this method instead of hard coding internal: true?
return when.all([
settings.read.call({ internal: true }, 'title'),
settings.read.call({ internal: true }, 'description'),
settings.read.call({ internal: true }, 'logo'),
settings.read.call({ internal: true }, 'cover')
settings.read('title'),
settings.read('description'),
settings.read('logo'),
settings.read('cover')
]).then(function (globals) {
// normalise the URL by removing any trailing slash
themeConfig.url = configUrl.replace(/\/$/, '');

View file

@ -148,9 +148,9 @@ function urlFor(context, data, absolute) {
// - post - a json object representing a post
// - absolute (optional, default:false) - boolean whether or not the url should be absolute
function urlForPost(settings, post, absolute) {
return settings.read.call({ internal: true }, 'permalinks').then(function (response) {
return settings.read('permalinks').then(function (response) {
var permalinks = response.settings[0];
return urlFor('post', {post: post, permalinks: permalinks}, absolute);
});
}

View file

@ -129,7 +129,7 @@ adminControllers = {
},
// frontend route for downloading a file
exportContent: function (req, res) {
api.db.exportContent.call({user: req.session.user}).then(function (exportData) {
api.db.exportContent({context: {user: req.session.user}}).then(function (exportData) {
// send a file to the client
res.set('Content-Disposition', 'attachment; filename="GhostData.json"');
res.json(exportData);
@ -260,10 +260,9 @@ adminControllers = {
password: password
}];
api.users.register({users: users}).then(function (apiResp) {
var user = apiResp.users[0];
api.settings.edit.call({user: 1}, 'email', email).then(function () {
api.users.register({users: users}).then(function (response) {
var user = response.users[0];
api.settings.edit({settings: [{key: 'email', value: email}]}, {context: {user: 1}}).then(function () {
var message = {
to: email,
subject: 'Your New Ghost Blog',

View file

@ -22,7 +22,7 @@ var moment = require('moment'),
staticPostPermalink = new Route(null, '/:slug/:edit?');
function getPostPage(options) {
return api.settings.read.call({ internal: true }, 'postsPerPage').then(function (response) {
return api.settings.read('postsPerPage').then(function (response) {
var postPP = response.settings[0],
postsPerPage = parseInt(postPP.value, 10);
@ -123,7 +123,7 @@ frontendControllers = {
// Render the page of posts
filters.doFilter('prePostsRender', page.posts).then(function (posts) {
api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) {
api.settings.read({key: 'activeTheme', context: {internal: true}}).then(function (response) {
var activeTheme = response.settings[0],
paths = config().paths.availableThemes[activeTheme.value],
view = paths.hasOwnProperty('tag.hbs') ? 'tag' : 'index',
@ -148,7 +148,7 @@ frontendControllers = {
editFormat,
usingStaticPermalink = false;
api.settings.read.call({ internal: true }, 'permalinks').then(function (response) {
api.settings.read('permalinks').then(function (response) {
var permalink = response.settings[0],
postLookup;
@ -203,7 +203,7 @@ frontendControllers = {
setReqCtx(req, post);
filters.doFilter('prePostsRender', post).then(function (post) {
api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) {
api.settings.read({key: 'activeTheme', context: {internal: true}}).then(function (response) {
var activeTheme = response.settings[0],
paths = config().paths.availableThemes[activeTheme.value],
view = template.getThemeViewForPost(paths, post);
@ -282,9 +282,9 @@ frontendControllers = {
}
return when.settle([
api.settings.read.call({ internal: true }, 'title'),
api.settings.read.call({ internal: true }, 'description'),
api.settings.read.call({ internal: true }, 'permalinks')
api.settings.read('title'),
api.settings.read('description'),
api.settings.read('permalinks')
]).then(function (result) {
var options = {};

View file

@ -112,7 +112,7 @@ function importUsers(ops, tableData, transaction) {
// don't override the users credentials
tableData = stripProperties(['id', 'email', 'password'], tableData);
tableData[0].id = 1;
ops.push(models.User.edit(tableData[0], {user: 1, transacting: transaction})
ops.push(models.User.edit(tableData[0], {id: 1, user: 1, transacting: transaction})
// add pass-through error handling so that bluebird doesn't think we've dropped it
.otherwise(function (error) { return when.reject(error); }));
}
@ -157,9 +157,9 @@ function importApps(ops, tableData, transaction) {
// var appsData = tableData.apps,
// appSettingsData = tableData.app_settings,
// appName;
//
//
// appSettingsData = stripProperties(['id'], appSettingsData);
//
//
// _.each(appSettingsData, function (appSetting) {
// // Find app to attach settings to
// appName = _.find(appsData, function (app) {

View file

@ -389,7 +389,7 @@ coreHelpers.body_class = function (options) {
classes.push('page');
}
return api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) {
return api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) {
var activeTheme = response.settings[0],
paths = config().paths.availableThemes[activeTheme.value],
view;
@ -532,8 +532,8 @@ coreHelpers.meta_description = function (options) {
coreHelpers.e = function (key, defaultString, options) {
var output;
when.all([
api.settings.read.call({ internal: true }, 'defaultLang'),
api.settings.read.call({ internal: true }, 'forceI18n')
api.settings.read('defaultLang'),
api.settings.read('forceI18n')
]).then(function (values) {
if (values[0].settings.value === 'en' &&
_.isEmpty(options.hash) &&

View file

@ -52,7 +52,7 @@ function doFirstRun() {
}
function initDbHashAndFirstRun() {
return api.settings.read.call({ internal: true }, 'dbHash').then(function (response) {
return api.settings.read({key: 'dbHash', context: {internal: true}}).then(function (response) {
var hash = response.settings[0].value,
initHash;
@ -60,10 +60,11 @@ function initDbHashAndFirstRun() {
if (dbHash === null) {
initHash = uuid.v4();
return api.settings.edit.call({ internal: true }, 'dbHash', initHash).then(function (response) {
dbHash = response.settings[0].value;
return dbHash;
}).then(doFirstRun);
return api.settings.edit({settings: [{key: 'dbHash', value: initHash}]}, {context: {internal: true}})
.then(function (response) {
dbHash = response.settings[0].value;
return dbHash;
}).then(doFirstRun);
}
return dbHash;

View file

@ -106,7 +106,7 @@ GhostMailer.prototype.send = function (payload) {
return when.reject(new Error('Email Error: Incomplete message data.'));
}
return api.settings.read.call({ internal: true }, 'email').then(function (response) {
return api.settings.read('email').then(function (response) {
var email = response.settings[0],
to = message.to || email.value;

View file

@ -39,7 +39,7 @@ function ghostLocals(req, res, next) {
if (res.isAdmin) {
res.locals.csrfToken = req.csrfToken();
when.all([
api.users.read.call({user: req.session.user}, {id: req.session.user}),
api.users.read({id: req.session.user}, {context: {user: req.session.user}}),
api.notifications.browse()
]).then(function (values) {
var currentUser = values[0].users[0],
@ -150,9 +150,9 @@ function manageAdminAndTheme(req, res, next) {
expressServer.enable(expressServer.get('activeTheme'));
expressServer.disable('admin');
}
api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) {
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) {
var activeTheme = response.settings[0];
// Check if the theme changed
if (activeTheme.value !== expressServer.get('activeTheme')) {
// Change theme

View file

@ -170,7 +170,7 @@ var middleware = {
// to allow unit testing
forwardToExpressStatic: function (req, res, next) {
api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) {
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) {
var activeTheme = response.settings[0];
// For some reason send divides the max age number by 1000
express['static'](path.join(config().paths.themePath, activeTheme.value), {maxAge: ONE_HOUR_MS})(req, res, next);

View file

@ -63,18 +63,20 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
},
creating: function (newObj, attr, options) {
var user = options.context && options.context.user ? options.context.user : 1;
if (!this.get('created_by')) {
this.set('created_by', options.user);
this.set('created_by', user);
}
},
saving: function (newObj, attr, options) {
var user = options.context && options.context.user ? options.context.user : 1;
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes());
// Store the previous attributes so we can tell what was updated later
this._updatedAttributes = newObj.previousAttributes();
this.set('updated_by', options.user);
this.set('updated_by', user);
},
// Base prototype properties will go here
@ -153,8 +155,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
}
}, {
// ## Model Data Functions
// ## Data Utility Functions
/**
* Returns an array of keys permitted in every method's `options` hash.
@ -191,6 +192,8 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
return filteredOptions;
},
// ## Model Data Functions
/**
* ### Find All
* Naive find all fetches all the data for a particular model
@ -219,6 +222,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
findOne: function (data, options) {
data = this.filterData(data);
options = this.filterOptions(options, 'findOne');
// We pass include to forge so that toJSON has access
return this.forge(data, {include: options.include}).fetch(options);
},
@ -230,9 +234,11 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
* @return {Promise(ghostBookshelf.Model)} Edited Model
*/
edit: function (data, options) {
var id = options.id;
data = this.filterData(data);
options = this.filterOptions(options, 'edit');
return this.forge({id: data.id}).fetch(options).then(function (object) {
return this.forge({id: id}).fetch(options).then(function (object) {
if (object) {
return object.save(data, options);
}
@ -250,11 +256,8 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
data = this.filterData(data);
options = this.filterOptions(options, 'add');
var instance = this.forge(data);
// We allow you to disable timestamps
// when importing posts so that
// the new posts `updated_at` value
// is the same as the import json blob.
// More details refer to https://github.com/TryGhost/Ghost/issues/1696
// We allow you to disable timestamps when importing posts so that the new posts `updated_at` value is the same
// as the import json blob. More details refer to https://github.com/TryGhost/Ghost/issues/1696
if (options.importing) {
instance.hasTimestamps = false;
}
@ -264,13 +267,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
/**
* ### Destroy
* Naive destroy
* @param {Object} data
* @param {Object} options (optional)
* @return {Promise(ghostBookshelf.Model)} Empty Model
*/
destroy: function (data, options) {
destroy: function (options) {
var id = options.id;
options = this.filterOptions(options, 'destroy');
return this.forge({id: data}).destroy(options);
return this.forge({id: id}).destroy(options);
},
/**

View file

@ -25,12 +25,12 @@ module.exports = {
return self.Post.findAll().then(function (posts) {
return when.all(_.map(posts.toJSON(), function (post) {
return self.Post.destroy(post.id);
return self.Post.destroy({id: post.id});
}));
}).then(function () {
return self.Tag.findAll().then(function (tags) {
return when.all(_.map(tags.toJSON(), function (tag) {
return self.Tag.destroy(tag.id);
return self.Tag.destroy({id: tag.id});
}));
});
});

View file

@ -44,7 +44,8 @@ Post = ghostBookshelf.Model.extend({
/*jshint unused:false*/
var self = this,
tagsToCheck,
i;
i,
user = options.context && options.context.user ? options.context.user : 1;
options = options || {};
// keep tags for 'saved' event and deduplicate upper/lowercase tags
@ -75,7 +76,7 @@ Post = ghostBookshelf.Model.extend({
this.set('published_at', new Date());
}
// This will need to go elsewhere in the API layer.
this.set('published_by', options.user);
this.set('published_by', user);
}
if (this.hasChanged('slug') || !this.get('slug')) {
@ -93,9 +94,11 @@ Post = ghostBookshelf.Model.extend({
/*jshint unused:false*/
options = options || {};
var user = options.context && options.context.user ? options.context.user : 1;
// set any dynamic default properties
if (!this.get('author_id')) {
this.set('author_id', options.user);
this.set('author_id', user);
}
ghostBookshelf.Model.prototype.creating.call(this, newPage, attr, options);
@ -105,7 +108,7 @@ Post = ghostBookshelf.Model.extend({
* ### updateTags
* Update tags that are attached to a post. Create any tags that don't already exist.
* @param {Object} newPost
* @param {Object} attr
* @param {Object} attr
* @param {Object} options
* @return {Promise(ghostBookshelf.Models.Post)} Updated Post model
*/
@ -243,8 +246,14 @@ Post = ghostBookshelf.Model.extend({
return filteredData;
},
// #### findAll
// Extends base model findAll to eager-fetch author and user relationships.
// ## Model Data Functions
/**
* ### Find All
*
* @param options
* @returns {*}
*/
findAll: function (options) {
options = options || {};
options.withRelated = _.union([ 'tags', 'fields' ], options.include);
@ -252,24 +261,24 @@ Post = ghostBookshelf.Model.extend({
},
// #### findPage
// Find results by page - returns an object containing the
// information about the request (page, limit), along with the
// info needed for pagination (pages, total).
// **response:**
// {
// posts: [
// {...}, {...}, {...}
// ],
// page: __,
// limit: __,
// pages: __,
// total: __
// }
/*
/**
* #### findPage
* Find results by page - returns an object containing the
* information about the request (page, limit), along with the
* info needed for pagination (pages, total).
*
* **response:**
*
* {
* posts: [
* {...}, {...}, {...}
* ],
* page: __,
* limit: __,
* pages: __,
* total: __
* }
*
* @params {Object} options
*/
findPage: function (options) {
@ -377,22 +386,23 @@ Post = ghostBookshelf.Model.extend({
meta = {},
data = {};
pagination['page'] = parseInt(options.page, 10);
pagination['limit'] = options.limit;
pagination['pages'] = calcPages === 0 ? 1 : calcPages;
pagination['total'] = totalPosts;
pagination['next'] = null;
pagination['prev'] = null;
pagination.page = parseInt(options.page, 10);
pagination.limit = options.limit;
pagination.pages = calcPages === 0 ? 1 : calcPages;
pagination.total = totalPosts;
pagination.next = null;
pagination.prev = null;
// Pass include to each model so that toJSON works correctly
if (options.include) {
_.each(postCollection.models, function (item) {
item.include = options.include;
});
}
data['posts'] = postCollection.toJSON();
data['meta'] = meta;
meta['pagination'] = pagination;
data.posts = postCollection.toJSON();
data.meta = meta;
meta.pagination = pagination;
if (pagination.pages > 1) {
if (pagination.page === 1) {
@ -406,9 +416,9 @@ Post = ghostBookshelf.Model.extend({
}
if (tagInstance) {
meta['filters'] = {};
meta.filters = {};
if (!tagInstance.isNew()) {
meta.filters['tags'] = [tagInstance.toJSON()];
meta.filters.tags = [tagInstance.toJSON()];
}
}
@ -417,59 +427,76 @@ Post = ghostBookshelf.Model.extend({
.catch(errors.logAndThrowError);
},
// #### findOne
// Extends base model read to eager-fetch author and user relationships.
findOne: function (args, options) {
/**
* ### Find One
* @extends ghostBookshelf.Model.findOne to handle post status
* **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
*/
findOne: function (data, options) {
options = options || {};
args = _.extend({
data = _.extend({
status: 'published'
}, args || {});
}, data || {});
if (args.status === 'all') {
delete args.status;
if (data.status === 'all') {
delete data.status;
}
// Add related objects
options.withRelated = _.union([ 'tags', 'fields' ], options.include);
return ghostBookshelf.Model.findOne.call(this, args, options);
return ghostBookshelf.Model.findOne.call(this, data, options);
},
add: function (newPostData, options) {
/**
* ### Edit
* @extends ghostBookshelf.Model.edit to handle returning the full object and manage _updatedAttributes
* **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
*/
edit: function (data, options) {
var self = this;
options = options || {};
return ghostBookshelf.Model.add.call(this, newPostData, options).then(function (post) {
return self.findOne({status: 'all', id: post.id}, options);
});
},
edit: function (editedPost, options) {
var self = this;
options = options || {};
return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (post) {
if (post) {
return self.findOne({status: 'all', id: post.id}, options)
.then(function (found) {
return ghostBookshelf.Model.edit.call(this, data, options).then(function (post) {
return self.findOne({status: 'all', id: options.id}, options)
.then(function (found) {
if (found) {
// Pass along the updated attributes for checking status changes
found._updatedAttributes = post._updatedAttributes;
return found;
});
}
}
});
});
},
destroy: function (_identifier, options) {
/**
* ### Add
* @extends ghostBookshelf.Model.add to handle returning the full object
* **See:** [ghostBookshelf.Model.add](base.js.html#add)
*/
add: function (data, options) {
var self = this;
options = options || {};
return ghostBookshelf.Model.add.call(this, data, options).then(function (post) {
return self.findOne({status: 'all', id: post.id}, options);
});
},
/**
* ### Destroy
* @extends ghostBookshelf.Model.destroy to clean up tag relations
* **See:** [ghostBookshelf.Model.destroy](base.js.html#destroy)
*/
destroy: function (options) {
var id = options.id;
options = this.filterOptions(options, 'destroy');
return this.forge({id: _identifier}).fetch({withRelated: ['tags']}).then(function destroyTags(post) {
var tagIds = _.pluck(post.related('tags').toJSON(), 'id');
if (tagIds) {
return post.tags().detach(tagIds).then(function destroyPost() {
return post.destroy(options);
});
}
return post.destroy(options);
return this.forge({id: id}).fetch({withRelated: ['tags']}).then(function destroyTagsAndPost(post) {
return post.related('tags').detach().then(function () {
return post.destroy(options);
});
});
},

View file

@ -37,7 +37,7 @@ Role = ghostBookshelf.Model.extend({
}
return options;
},
}
});
Roles = ghostBookshelf.Collection.extend({

View file

@ -19,7 +19,7 @@ Session = ghostBookshelf.Model.extend({
/*jshint unused:false*/
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes());
},
}
}, {
destroyAll: function (options) {

View file

@ -80,23 +80,23 @@ Settings = ghostBookshelf.Model.extend({
return options;
},
findOne: function (_key) {
findOne: function (options) {
// Allow for just passing the key instead of attributes
if (!_.isObject(_key)) {
_key = { key: _key };
if (!_.isObject(options)) {
options = { key: options };
}
return when(ghostBookshelf.Model.findOne.call(this, _key));
return when(ghostBookshelf.Model.findOne.call(this, options));
},
edit: function (_data, options) {
edit: function (data, options) {
var self = this;
options = this.filterOptions(options, 'edit');
if (!Array.isArray(_data)) {
_data = [_data];
if (!Array.isArray(data)) {
data = [data];
}
return when.map(_data, function (item) {
return when.map(data, function (item) {
// Accept an array of models as input
if (item.toJSON) { item = item.toJSON(); }
if (!(_.isString(item.key) && item.key.length > 0)) {

View file

@ -57,7 +57,7 @@ Tag = ghostBookshelf.Model.extend({
}
return options;
},
}
});
Tags = ghostBookshelf.Collection.extend({

View file

@ -106,16 +106,20 @@ User = ghostBookshelf.Model.extend({
},
/**
* ## Add
* Naive user add
* @param {object} _user
*
* Hashes the password provided before saving to the database.
*
* @param {object} data
* @param {object} options
* @extends ghostBookshelf.Model.add to manage all aspects of user signup
* **See:** [ghostBookshelf.Model.add](base.js.html#Add)
*/
add: function (_user, options) {
add: function (data, options) {
var self = this,
// Clone the _user so we don't expose the hashed password unnecessarily
userData = this.filterData(_user);
userData = this.filterData(data);
options = this.filterOptions(options, 'add');
@ -133,7 +137,7 @@ User = ghostBookshelf.Model.extend({
}
}).then(function () {
// Generate a new password hash
return generatePasswordHash(_user.password);
return generatePasswordHash(data.password);
}).then(function (hash) {
// Assign the hashed password
userData.password = hash;
@ -143,6 +147,7 @@ User = ghostBookshelf.Model.extend({
// Save the user with the hashed password
return ghostBookshelf.Model.add.call(self, userData, options);
}).then(function (addedUser) {
// Assign the userData to our created user so we can pass it back
userData = addedUser;
// Add this user to the admin role (assumes admin = role_id: 1)

View file

@ -35,16 +35,11 @@ function parseContext(context) {
parsed.internal = true;
}
// @TODO: Refactor canThis() references to pass { user: id } explicitly instead of primitives.
if (context && context.id) {
// Handle passing of just user.id string
parsed.user = context.id;
} else if (_.isNumber(context)) {
// Handle passing of just user id number
parsed.user = context;
} else if (_.isObject(context)) {
// Otherwise, use the new hotness { user: id, app: id } format
if (context && context.user) {
parsed.user = context.user;
}
if (context && context.app) {
parsed.app = context.app;
}

View file

@ -3,9 +3,11 @@ var admin = require('../controllers/admin'),
middleware = require('../middleware').middleware,
ONE_HOUR_S = 60 * 60,
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S;
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
module.exports = function (server) {
adminRoutes;
adminRoutes = function (server) {
// Have ember route look for hits first
// to prevent conflicts with pre-existing routes
server.get('/ghost/ember/*', admin.index);
@ -65,4 +67,6 @@ module.exports = function (server) {
res.redirect(subdir + '/ghost/');
});
server.get('/ghost/', admin.indexold);
};
};
module.exports = adminRoutes;

View file

@ -1,37 +1,43 @@
// # API routes
var middleware = require('../middleware').middleware,
api = require('../api');
api = require('../api'),
apiRoutes;
module.exports = function (server) {
// ### API routes
// #### Posts
server.get('/ghost/api/v0.1/posts', api.requestHandler(api.posts.browse));
server.post('/ghost/api/v0.1/posts', api.requestHandler(api.posts.add));
server.get('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.read));
server.put('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.edit));
server.del('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.destroy));
server.get('/ghost/api/v0.1/posts/slug/:title', middleware.authAPI, api.requestHandler(api.posts.generateSlug));
// #### Settings
server.get('/ghost/api/v0.1/settings/', api.requestHandler(api.settings.browse));
server.get('/ghost/api/v0.1/settings/:key/', api.requestHandler(api.settings.read));
server.put('/ghost/api/v0.1/settings/', api.requestHandler(api.settings.edit));
// #### Users
server.get('/ghost/api/v0.1/users/', api.requestHandler(api.users.browse));
server.get('/ghost/api/v0.1/users/:id/', api.requestHandler(api.users.read));
server.put('/ghost/api/v0.1/users/:id/', api.requestHandler(api.users.edit));
// #### Tags
server.get('/ghost/api/v0.1/tags/', api.requestHandler(api.tags.browse));
// #### Themes
server.get('/ghost/api/v0.1/themes/', api.requestHandler(api.themes.browse));
server.put('/ghost/api/v0.1/themes/:name', api.requestHandler(api.themes.edit));
// #### Notifications
server.del('/ghost/api/v0.1/notifications/:id', api.requestHandler(api.notifications.destroy));
server.post('/ghost/api/v0.1/notifications/', api.requestHandler(api.notifications.add));
server.get('/ghost/api/v0.1/notifications/', api.requestHandler(api.notifications.browse));
// #### Import/Export
server.get('/ghost/api/v0.1/db/', api.requestHandler(api.db.exportContent));
server.post('/ghost/api/v0.1/db/', middleware.busboy, api.requestHandler(api.db.importContent));
server.del('/ghost/api/v0.1/db/', api.requestHandler(api.db.deleteAllContent));
// #### Mail
server.post('/ghost/api/v0.1/mail', api.requestHandler(api.mail.send));
server.post('/ghost/api/v0.1/mail/test', api.requestHandler(api.mail.sendTest));
};
apiRoutes = function (server) {
// ## Posts
server.get('/ghost/api/v0.1/posts', api.http(api.posts.browse));
server.post('/ghost/api/v0.1/posts', api.http(api.posts.add));
server.get('/ghost/api/v0.1/posts/:id(\\d+)', api.http(api.posts.read));
server.get('/ghost/api/v0.1/posts/:slug([a-z-]+)', api.http(api.posts.read));
server.put('/ghost/api/v0.1/posts/:id', api.http(api.posts.edit));
server.del('/ghost/api/v0.1/posts/:id', api.http(api.posts.destroy));
server.get('/ghost/api/v0.1/posts/slug/:title', api.http(api.posts.generateSlug));
// ## Settings
server.get('/ghost/api/v0.1/settings/', api.http(api.settings.browse));
server.get('/ghost/api/v0.1/settings/:key/', api.http(api.settings.read));
server.put('/ghost/api/v0.1/settings/', api.http(api.settings.edit));
// ## Users
server.get('/ghost/api/v0.1/users/', api.http(api.users.browse));
server.get('/ghost/api/v0.1/users/:id/', api.http(api.users.read));
server.put('/ghost/api/v0.1/users/:id/', api.http(api.users.edit));
// ## Tags
server.get('/ghost/api/v0.1/tags/', api.http(api.tags.browse));
// ## Themes
server.get('/ghost/api/v0.1/themes/', api.http(api.themes.browse));
server.put('/ghost/api/v0.1/themes/:name', api.http(api.themes.edit));
// ## Notifications
server.del('/ghost/api/v0.1/notifications/:id', api.http(api.notifications.destroy));
server.post('/ghost/api/v0.1/notifications/', api.http(api.notifications.add));
server.get('/ghost/api/v0.1/notifications/', api.http(api.notifications.browse));
server.post('/ghost/api/v0.1/notifications/', api.http(api.notifications.add));
server.del('/ghost/api/v0.1/notifications/:id', api.http(api.notifications.destroy));
// ## DB
server.get('/ghost/api/v0.1/db/', api.http(api.db.exportContent));
server.post('/ghost/api/v0.1/db/', middleware.busboy, api.http(api.db.importContent));
server.del('/ghost/api/v0.1/db/', api.http(api.db.deleteAllContent));
// ## Mail
server.post('/ghost/api/v0.1/mail', api.http(api.mail.send));
server.post('/ghost/api/v0.1/mail/test', api.http(api.mail.sendTest));
};
module.exports = apiRoutes;

View file

@ -2,9 +2,11 @@ var frontend = require('../controllers/frontend'),
config = require('../config'),
ONE_HOUR_S = 60 * 60,
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S;
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
module.exports = function (server) {
frontendRoutes;
frontendRoutes = function (server) {
var subdir = config().paths.subdir;
// ### Frontend routes
@ -24,6 +26,6 @@ module.exports = function (server) {
server.get('/page/:page/', frontend.homepage);
server.get('/', frontend.homepage);
server.get('*', frontend.single);
};
};
module.exports = frontendRoutes;

View file

@ -50,9 +50,9 @@ function updateCheckData() {
ops = [],
mailConfig = config().mail;
ops.push(api.settings.read.call({ internal: true }, 'dbHash').otherwise(errors.rejectError));
ops.push(api.settings.read.call({ internal: true }, 'activeTheme').otherwise(errors.rejectError));
ops.push(api.settings.read.call({ internal: true }, 'activeApps')
ops.push(api.settings.read({context: {internal: true}, key: 'dbHash'}).otherwise(errors.rejectError));
ops.push(api.settings.read({context: {internal: true}, key: 'activeTheme'}).otherwise(errors.rejectError));
ops.push(api.settings.read({context: {internal: true}, key: 'activeApps'})
.then(function (response) {
var apps = response.settings[0];
try {
@ -64,7 +64,7 @@ function updateCheckData() {
return _.reduce(apps, function (memo, item) { return memo === '' ? memo + item : memo + ', ' + item; }, '');
}).otherwise(errors.rejectError));
ops.push(api.posts.browse().otherwise(errors.rejectError));
ops.push(api.users.browse.call({user: 1}).otherwise(errors.rejectError));
ops.push(api.users.browse({context: {user: 1}}).otherwise(errors.rejectError));
ops.push(nodefn.call(exec, 'npm -v').otherwise(errors.rejectError));
data.ghost_version = currentVersion;
@ -142,13 +142,21 @@ function updateCheckRequest() {
// 1. Updates the time we can next make a check
// 2. Checks if the version in the response is new, and updates the notification setting
function updateCheckResponse(response) {
var ops = [];
var ops = [],
internalContext = {context: {internal: true}};
ops.push(api.settings.edit.call({internal: true}, 'nextUpdateCheck', response.next_check)
.otherwise(errors.rejectError));
ops.push(api.settings.edit.call({internal: true}, 'displayUpdateNotification', response.version)
.otherwise(errors.rejectError));
ops.push(
api.settings.edit(
{settings: [{key: 'nextUpdateCheck', value: response.next_check}]},
internalContext
)
.otherwise(errors.rejectError),
api.settings.edit(
{settings: [{key: 'displayUpdateNotification', value: response.version}]},
internalContext
)
.otherwise(errors.rejectError)
);
return when.settle(ops).then(function (descriptors) {
descriptors.forEach(function (d) {
@ -171,7 +179,7 @@ function updateCheck() {
// No update check
deferred.resolve();
} else {
api.settings.read.call({ internal: true }, 'nextUpdateCheck').then(function (result) {
api.settings.read({context: {internal: true}, key: 'nextUpdateCheck'}).then(function (result) {
var nextUpdateCheck = result.settings[0];
if (nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
@ -191,7 +199,7 @@ function updateCheck() {
}
function showUpdateNotification() {
return api.settings.read.call({ internal: true }, 'displayUpdateNotification').then(function (response) {
return api.settings.read({context: {internal: true}, key: 'displayUpdateNotification'}).then(function (response) {
var display = response.settings[0];
// Version 0.4 used boolean to indicate the need for an update. This special case is

View file

@ -1,9 +1,11 @@
// Posts
var blanket = require("blanket")({
"pattern": ["/core/server/", "/core/clientold/", "/core/shared/"],
"data-cover-only": ["/core/server/", "/core/clientold/", "/core/shared/"]
}),
requireDir = require("require-dir");
requireDir("./unit");
requireDir("./integration");
requireDir("./functional/routes");

View file

@ -61,7 +61,7 @@ describe('Post API', function () {
if (err) {
return done(err);
}
csrfToken = res.text.match(pattern_meta)[1];
done();
});
@ -124,7 +124,7 @@ describe('Post API', function () {
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
done();
});
});
// Test bits of the API we don't use in the app yet to ensure the API behaves properly
@ -193,7 +193,7 @@ describe('Post API', function () {
// ## Read
describe('Read', function () {
it('can retrieve a post', function (done) {
it('can retrieve a post by id', function (done) {
request.get(testUtils.API.getApiQuery('posts/1/'))
.end(function (err, res) {
if (err) {
@ -207,6 +207,32 @@ describe('Post API', function () {
jsonResponse.should.exist;
jsonResponse.posts.should.exist;
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
jsonResponse.posts[0].id.should.equal(1);
jsonResponse.posts[0].page.should.eql(0);
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);
jsonResponse.posts[0].author.should.be.a.Number;
jsonResponse.posts[0].created_by.should.be.a.Number;
jsonResponse.posts[0].tags[0].should.be.a.Number;
done();
});
});
it('can retrieve a post by slug', function (done) {
request.get(testUtils.API.getApiQuery('posts/welcome-to-ghost/'))
.end(function (err, res) {
if (err) {
return done(err);
}
res.should.have.status(200);
should.not.exist(res.headers['x-cache-invalidate']);
res.should.be.json;
var jsonResponse = res.body;
jsonResponse.should.exist;
jsonResponse.posts.should.exist;
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
jsonResponse.posts[0].slug.should.equal('welcome-to-ghost');
jsonResponse.posts[0].page.should.eql(0);
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);
@ -477,7 +503,6 @@ describe('Post API', function () {
});
});
it('can change a static page to a post', function (done) {
request.get(testUtils.API.getApiQuery('posts/7/'))
.end(function (err, res) {
@ -501,11 +526,11 @@ describe('Post API', function () {
}
var putBody = res.body;
_.has(res.headers, 'x-cache-invalidate').should.equal(false);
res.should.be.json;
putBody.should.exist;
putBody.posts[0].page.should.eql(changedValue);
testUtils.API.checkResponse(putBody.posts[0], 'post');
done();
});
@ -605,10 +630,6 @@ describe('Post API', function () {
});
});
});
// ## delete
describe('Delete', function () {
it('can\'t edit non existent post', function (done) {
request.get(testUtils.API.getApiQuery('posts/1/'))
.end(function (err, res) {
@ -630,7 +651,6 @@ describe('Post API', function () {
return done(err);
}
var putBody = res.body;
_.has(res.headers, 'x-cache-invalidate').should.equal(false);
res.should.be.json;
jsonResponse = res.body;
@ -641,6 +661,10 @@ describe('Post API', function () {
});
});
});
// ## delete
describe('Delete', function () {
it('can delete a post', function (done) {
var deletePostId = 1;
request.del(testUtils.API.getApiQuery('posts/' + deletePostId + '/'))
@ -829,9 +853,5 @@ describe('Post API', function () {
});
});
});
});
});

View file

@ -214,7 +214,7 @@ describe('Settings API', function () {
newValue = 'new value';
jsonResponse.should.exist;
should.exist(jsonResponse.settings);
jsonResponse.settings.push({ key: 'testvalue', value: newValue });
jsonResponse.settings = [{ key: 'testvalue', value: newValue }];
request.put(testUtils.API.getApiQuery('settings/'))
.set('X-CSRF-Token', csrfToken)

View file

@ -42,7 +42,7 @@ describe('User API', function () {
pattern_meta.should.exist;
csrfToken = res.text.match(pattern_meta)[1];
process.nextTick(function() {
process.nextTick(function () {
request.post('/ghost/signin/')
.set('X-CSRF-Token', csrfToken)
.send({email: user.email, password: user.password})
@ -144,13 +144,16 @@ describe('User API', function () {
}
var jsonResponse = res.body,
changedValue = 'joe-bloggs.ghost.org';
changedValue = 'joe-bloggs.ghost.org',
dataToSend;
jsonResponse.users[0].should.exist;
jsonResponse.users[0].website = changedValue;
testUtils.API.checkResponse(jsonResponse.users[0], 'user');
dataToSend = { users: [{website: changedValue}]};
request.put(testUtils.API.getApiQuery('users/me/'))
.set('X-CSRF-Token', csrfToken)
.send(jsonResponse)
.send(dataToSend)
.expect(200)
.end(function (err, res) {
if (err) {
@ -162,7 +165,7 @@ describe('User API', function () {
res.should.be.json;
putBody.users[0].should.exist;
putBody.users[0].website.should.eql(changedValue);
putBody.users[0].email.should.eql(jsonResponse.users[0].email);
testUtils.API.checkResponse(putBody.users[0], 'user');
done();
});
@ -195,6 +198,4 @@ describe('User API', function () {
});
});
});

View file

@ -37,7 +37,7 @@ describe('DB API', function () {
it('delete all content', function (done) {
permissions.init().then(function () {
return dbAPI.deleteAllContent.call({user: 1});
return dbAPI.deleteAllContent({context: {user: 1}});
}).then(function (result) {
should.exist(result.db);
result.db.should.be.instanceof(Array);
@ -61,12 +61,12 @@ describe('DB API', function () {
it('delete all content is denied', function (done) {
permissions.init().then(function () {
return dbAPI.deleteAllContent.call({user: 2});
return dbAPI.deleteAllContent({context: {user: 2}});
}).then(function (){
done(new Error("Delete all content is not denied for editor."));
}, function (error) {
error.type.should.eql('NoPermissionError');
return dbAPI.deleteAllContent.call({user: 3});
return dbAPI.deleteAllContent({context: {user: 3}});
}).then(function (){
done(new Error("Delete all content is not denied for author."));
}, function (error) {
@ -82,12 +82,12 @@ describe('DB API', function () {
it('export content is denied', function (done) {
permissions.init().then(function () {
return dbAPI.exportContent.call({user: 2});
return dbAPI.exportContent({context: {user: 2}});
}).then(function (){
done(new Error("Export content is not denied for editor."));
}, function (error) {
error.type.should.eql('NoPermissionError');
return dbAPI.exportContent.call({user: 3});
return dbAPI.exportContent({context: {user: 3}});
}).then(function (){
done(new Error("Export content is not denied for author."));
}, function (error) {
@ -103,12 +103,12 @@ describe('DB API', function () {
it('import content is denied', function (done) {
permissions.init().then(function () {
return dbAPI.importContent.call({user: 2});
return dbAPI.importContent({context: {user: 2}});
}).then(function (result){
done(new Error("Import content is not denied for editor."));
}, function (error) {
error.type.should.eql('NoPermissionError');
return dbAPI.importContent.call({user: 3});
return dbAPI.importContent({context: {user: 3}});
}).then(function (result){
done(new Error("Import content is not denied for author."));
}, function (error) {

View file

@ -1,4 +1,4 @@
/*globals describe, before, beforeEach, afterEach, it */
/*globals describe, before, beforeEach, afterEach, it */
var testUtils = require('../../utils'),
should = require('should'),

View file

@ -17,7 +17,14 @@ describe('Settings API', function () {
internal: true
},
callApiWithContext = function (context, method) {
return SettingsAPI[method].apply(context, _.toArray(arguments).slice(2));
var args = _.toArray(arguments),
options = args[args.length - 1];
if (_.isObject(options)) {
options.context = _.clone(context);
}
return SettingsAPI[method].apply({}, args.slice(2));
},
getErrorDetails = function (done) {
return function (err) {
@ -58,7 +65,7 @@ describe('Settings API', function () {
});
it('can browse', function (done) {
return callApiWithContext(defaultContext, 'browse', 'blog').then(function (results) {
return callApiWithContext(defaultContext, 'browse', {}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'settings');
results.settings.length.should.be.above(0);
@ -71,8 +78,23 @@ describe('Settings API', function () {
}).catch(getErrorDetails(done));
});
it('returns core settings for internal requests when browsing', function (done){
return callApiWithContext(internalContext, 'browse', 'blog').then(function (results) {
it('can browse by type', function (done) {
return callApiWithContext(defaultContext, 'browse', {type: 'blog'}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'settings');
results.settings.length.should.be.above(0);
testUtils.API.checkResponse(results.settings[0], 'setting');
// Check for a core setting
should.not.exist(_.find(results.settings, function (setting) { return setting.type === 'core'; }));
done();
}).catch(getErrorDetails(done));
});
it('returns core settings for internal requests when browsing', function (done) {
return callApiWithContext(internalContext, 'browse', {}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'settings');
results.settings.length.should.be.above(0);
@ -82,11 +104,11 @@ describe('Settings API', function () {
should.exist(_.find(results.settings, function (setting) { return setting.type === 'core'; }));
done();
}).catch(getErrorDetails(done));
}).catch(getErrorDetails(done));
});
it('can read by string', function (done) {
return callApiWithContext(defaultContext, 'read', 'title').then(function (response) {
it('can read blog settings by string', function (done) {
return SettingsAPI.read('title').then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
@ -97,19 +119,17 @@ describe('Settings API', function () {
});
it('cannot read core settings if not an internal request', function (done) {
return callApiWithContext(defaultContext, 'read', 'databaseVersion').then(function (response) {
return callApiWithContext(defaultContext, 'read', {key: 'databaseVersion'}).then(function (response) {
done(new Error('Allowed to read databaseVersion with external request'));
}).catch(function (err) {
should.exist(err);
err.message.should.equal('Attempted to access core setting on external request');
}).catch(function (error) {
should.exist(error);
error.type.should.eql('NoPermissionError');
done();
});
});
it('can read core settings if an internal request', function (done) {
return callApiWithContext(internalContext, 'read', 'databaseVersion').then(function (response) {
return callApiWithContext(internalContext, 'read', {key: 'databaseVersion'}).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
@ -131,37 +151,40 @@ describe('Settings API', function () {
});
it('can edit', function (done) {
return callApiWithContext(defaultContext, 'edit', { settings: [{ key: 'title', value: 'UpdatedGhost'}]}).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
return callApiWithContext(defaultContext, 'edit', {settings: [{ key: 'title', value: 'UpdatedGhost'}]}, {})
.then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
done();
}).catch(done);
});
it('can edit, by key/value', function (done) {
return callApiWithContext(defaultContext, 'edit', 'title', 'UpdatedGhost').then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
done();
}).catch(getErrorDetails(done));
done();
}).catch(done);
});
it('cannot edit a core setting if not an internal request', function (done) {
return callApiWithContext(defaultContext, 'edit', 'databaseVersion', '999').then(function (response) {
done(new Error('Allowed to edit a core setting as external request'));
}).catch(function (err) {
should.exist(err);
return callApiWithContext(defaultContext, 'edit', {settings: [{ key: 'databaseVersion', value: '999'}]}, {})
.then(function () {
done(new Error('Allowed to edit a core setting as external request'));
}).catch(function (err) {
should.exist(err);
err.message.should.equal('Attempted to access core setting on external request');
err.type.should.eql('NoPermissionError');
done();
});
done();
});
});
it('can edit a core setting with an internal request', function (done) {
return callApiWithContext(internalContext, 'edit', {settings: [{ key: 'databaseVersion', value: '999'}]}, {})
.then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
done();
}).catch(done);
});
it('ensures values are stringified before saving to database', function (done) {

View file

@ -8,7 +8,7 @@ var _ = require('lodash'),
// Stuff we are testing
permissions = require('../../../server/permissions'),
settings = require('../../../server/api/settings'),
SettingsAPI = require('../../../server/api/settings'),
ThemeAPI = rewire('../../../server/api/themes');
describe('Themes API', function () {
@ -26,12 +26,15 @@ describe('Themes API', function () {
testUtils.initData().then(function () {
return testUtils.insertDefaultFixtures();
}).then(function () {
return SettingsAPI.updateSettingsCache();
}).then(function () {
return permissions.init();
}).then(function () {
sandbox = sinon.sandbox.create();
// Override settings.read for activeTheme
settingsReadStub = sandbox.stub(settings, 'read', function () {
settingsReadStub = sandbox.stub(SettingsAPI, 'read', function () {
return when({ settings: [{value: 'casper'}] });
});
@ -67,14 +70,14 @@ describe('Themes API', function () {
_.extend(configStub, config);
ThemeAPI.__set__('config', configStub);
ThemeAPI.browse.call({user: 1}).then(function (result) {
ThemeAPI.browse({context: {user: 1}}).then(function (result) {
should.exist(result);
result.themes.length.should.be.above(0);
testUtils.API.checkResponse(result.themes[0], 'theme');
done();
}, function (error) {
}).catch(function (error) {
done(new Error(JSON.stringify(error)));
})
});
});
it('can edit', function (done) {
@ -84,15 +87,15 @@ describe('Themes API', function () {
_.extend(configStub, config);
ThemeAPI.__set__('config', configStub);
ThemeAPI.edit.call({user: 1}, {themes: [{uuid: 'rasper', active: true }]}).then(function (result) {
ThemeAPI.edit({themes: [{uuid: 'rasper', active: true }]}, {context: {user: 1}}).then(function (result) {
should.exist(result);
should.exist(result.themes);
result.themes.length.should.be.above(0);
testUtils.API.checkResponse(result.themes[0], 'theme');
result.themes[0].uuid.should.equal('rasper');
done();
}, function (error) {
}).catch(function (error) {
done(new Error(JSON.stringify(error)));
})
});
})
});

View file

@ -2,6 +2,8 @@
var testUtils = require('../../utils'),
should = require('should'),
permissions = require('../../../server/permissions'),
// Stuff we are testing
UsersAPI = require('../../../server/api/users');
@ -22,11 +24,12 @@ describe('Users API', function () {
describe('No User', function () {
beforeEach(function (done) {
testUtils.initData().then(function () {
return permissions.init();
}).then(function () {
done();
}).catch(done);
});
it('can add with internal user', function (done) {
UsersAPI.register({ users: [{
'name': 'Hello World',
@ -52,13 +55,15 @@ describe('Users API', function () {
return testUtils.insertEditorUser();
}).then(function () {
return testUtils.insertAuthorUser();
}).then(function () {
return permissions.init();
}).then(function () {
done();
}).catch(done);
});
it('admin can browse', function (done) {
UsersAPI.browse.call({user: 1}).then(function (results) {
UsersAPI.browse({context: {user: 1}}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'users');
should.exist(results.users);
@ -71,7 +76,7 @@ describe('Users API', function () {
});
it('editor can browse', function (done) {
UsersAPI.browse.call({user: 2}).then(function (results) {
UsersAPI.browse({context: {user: 2}}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'users');
should.exist(results.users);
@ -84,7 +89,7 @@ describe('Users API', function () {
});
it('author can browse', function (done) {
UsersAPI.browse.call({user: 3}).then(function (results) {
UsersAPI.browse({context: {user: 3}}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'users');
should.exist(results.users);
@ -105,7 +110,7 @@ describe('Users API', function () {
});
it('admin can read', function (done) {
UsersAPI.read.call({user: 1}, {id: 1}).then(function (results) {
UsersAPI.read({id: 1, context: {user: 1}}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'users');
results.users[0].id.should.eql(1);
@ -115,7 +120,7 @@ describe('Users API', function () {
});
it('editor can read', function (done) {
UsersAPI.read.call({user: 2}, {id: 1}).then(function (results) {
UsersAPI.read({id: 1, context: {user: 2}}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'users');
results.users[0].id.should.eql(1);
@ -125,7 +130,7 @@ describe('Users API', function () {
});
it('author can read', function (done) {
UsersAPI.read.call({user: 3}, {id: 1}).then(function (results) {
UsersAPI.read({id: 1, context: {user: 3}}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'users');
results.users[0].id.should.eql(1);
@ -145,7 +150,7 @@ describe('Users API', function () {
});
it('admin can edit', function (done) {
UsersAPI.edit.call({user: 1}, {users: [{id: 1, name: 'Joe Blogger'}]}).then(function (response) {
UsersAPI.edit({users: [{name: 'Joe Blogger'}]}, {id: 1, context: {user: 1}}).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'users');
response.users.should.have.length(1);
@ -157,7 +162,7 @@ describe('Users API', function () {
});
it('editor can edit', function (done) {
UsersAPI.edit.call({user: 2}, {users: [{id: 1, name: 'Joe Blogger'}]}).then(function (response) {
UsersAPI.edit({users: [{name: 'Joe Blogger'}]}, {id: 1, context: {user: 2}}).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'users');
response.users.should.have.length(1);
@ -170,13 +175,13 @@ describe('Users API', function () {
it('author can edit only self', function (done) {
// Test author cannot edit admin user
UsersAPI.edit.call({user: 3}, {users: [{id: 1, name: 'Joe Blogger'}]}).then(function () {
UsersAPI.edit({users: [{name: 'Joe Blogger'}]}, {id: 1, context: {user: 3}}).then(function () {
done(new Error('Author should not be able to edit account which is not their own'));
}).catch(function (error) {
error.code.should.eql(403);
error.type.should.eql('NoPermissionError');
}).finally(function () {
// Next test that author CAN edit self
return UsersAPI.edit.call({user: 3}, {users: [{id: 3, name: 'Timothy Bogendath'}]})
return UsersAPI.edit({users: [{name: 'Timothy Bogendath'}]}, {id: 3, context: {user: 3}})
.then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'users');

View file

@ -86,19 +86,20 @@ describe('App Model', function () {
}).catch(done);
});
it("can delete", function (done) {
AppModel.findOne({id: 1}).then(function (foundApp) {
it("can destroy", function (done) {
var firstApp = {id: 1};
AppModel.findOne(firstApp).then(function (foundApp) {
should.exist(foundApp);
foundApp.attributes.id.should.equal(firstApp.id);
return AppModel.destroy(1);
}).then(function () {
return AppModel.findAll();
}).then(function (foundApp) {
var hasRemovedId = foundApp.any(function (foundApp) {
return foundApp.id === 1;
});
return AppModel.destroy(firstApp);
}).then(function (response) {
response.toJSON().should.be.empty;
hasRemovedId.should.equal(false);
return AppModel.findOne(firstApp);
}).then(function (newResults) {
should.equal(newResults, null);
done();
}).catch(done);

View file

@ -80,19 +80,19 @@ describe('Permission Model', function () {
}).catch(done);
});
it('can delete', function (done) {
PermissionModel.findOne({id: 1}).then(function (foundPermission) {
it('can destroy', function (done) {
var firstPermission = {id: 1};
PermissionModel.findOne(firstPermission).then(function (foundPermission) {
should.exist(foundPermission);
foundPermission.attributes.id.should.equal(firstPermission.id);
return PermissionModel.destroy(1);
}).then(function () {
return PermissionModel.findAll();
}).then(function (foundPermissions) {
var hasRemovedId = foundPermissions.any(function (permission) {
return permission.id === 1;
});
hasRemovedId.should.equal(false);
return PermissionModel.destroy(firstPermission);
}).then(function (response) {
response.toJSON().should.be.empty;
return PermissionModel.findOne(firstPermission);
}).then(function (newResults) {
should.equal(newResults, null);
done();
}).catch(done);

View file

@ -35,19 +35,80 @@ describe('Post Model', function () {
}).catch(done);
});
function checkFirstPostData(firstPost) {
should.not.exist(firstPost.author_id);
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.tags.should.be.an.Array;
firstPost.author.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
firstPost.created_by.should.be.an.Object;
firstPost.updated_by.should.be.an.Object;
firstPost.published_by.should.be.an.Object;
firstPost.created_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.tags[0].name.should.equal('Getting Started');
}
it('can findAll', function (done) {
PostModel.findAll().then(function (results) {
should.exist(results);
results.length.should.be.above(1);
// should be in published_at, DESC order
// model and API differ here - need to fix
//results.models[0].attributes.published_at.should.be.above(results.models[1].attributes.published_at);
done();
}).catch(done);
});
it('can findAll, returning all related data', function (done) {
var firstPost;
PostModel.findAll({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
should.exist(results);
results.length.should.be.above(0);
firstPost = results.models[0].toJSON();
checkFirstPostData(firstPost);
done();
}).catch(done);
});
it('can findPage (default)', function (done) {
PostModel.findPage().then(function (results) {
should.exist(results);
results.meta.pagination.page.should.equal(1);
results.meta.pagination.limit.should.equal(15);
results.meta.pagination.pages.should.equal(1);
results.posts.length.should.equal(5);
done();
}).catch(done);
});
it('can findPage, returning all related data', function (done) {
var firstPost;
PostModel.findPage({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
should.exist(results);
results.meta.pagination.page.should.equal(1);
results.meta.pagination.limit.should.equal(15);
results.meta.pagination.pages.should.equal(1);
results.posts.length.should.equal(5);
firstPost = results.posts[0];
checkFirstPostData(firstPost);
done();
}).catch(done);
});
it('can findOne', function (done) {
var firstPost;
@ -66,50 +127,31 @@ describe('Post Model', function () {
}).catch(done);
});
it('can findAll, returning author and field data', function (done) {
var firstPost;
PostModel.findAll({include: ['author_id', 'fields']}).then(function (results) {
should.exist(results);
results.length.should.be.above(0);
firstPost = results.models[0].toJSON();
should.not.exist(firstPost.author_id);
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.author.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
done();
}).catch(done);
});
it('can findOne, returning author and field data', function (done) {
it('can findOne, returning all related data', function (done) {
var firstPost;
// TODO: should take author :-/
PostModel.findOne({}, {include: ['author_id', 'fields']}).then(function (result) {
should.exist(result);
firstPost = result.toJSON();
PostModel.findOne({}, {include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (result) {
should.exist(result);
firstPost = result.toJSON();
should.not.exist(firstPost.author_id);
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.author.name.should.equal(testUtils.DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
checkFirstPostData(firstPost);
done();
}).catch(done);
done();
}).catch(done);
});
it('can edit', function (done) {
var firstPost;
var firstPost = 1;
PostModel.findAll().then(function (results) {
PostModel.findOne({id: firstPost}).then(function (results) {
var post;
should.exist(results);
results.length.should.be.above(0);
firstPost = results.models[0];
post = results.toJSON();
post.id.should.equal(firstPost);
post.title.should.not.equal('new title');
return PostModel.edit({id: firstPost.id, title: 'new title'});
return PostModel.edit({title: 'new title'}, {id: firstPost});
}).then(function (edited) {
should.exist(edited);
edited.attributes.title.should.equal('new title');
@ -118,6 +160,7 @@ describe('Post Model', function () {
}).catch(done);
});
it('can add, defaults are all correct', function (done) {
var createdPostUpdatedDate,
newPost = testUtils.DataGenerator.forModel.posts[2],
@ -330,18 +373,32 @@ describe('Post Model', function () {
}).catch(done);
});
it('can delete', function (done) {
var firstPostId;
PostModel.findAll().then(function (results) {
should.exist(results);
results.length.should.be.above(0);
firstPostId = results.models[0].id;
it('can destroy', function (done) {
// We're going to try deleting post id 1 which also has tag id 1
var firstItemData = {id: 1};
return PostModel.destroy(firstPostId);
}).then(function () {
return PostModel.findOne({id: firstPostId});
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
post.id.should.equal(firstItemData.id);
post.tags.should.have.length(1);
post.tags[0].should.equal(firstItemData.id);
// Destroy the post
return PostModel.destroy(firstItemData);
}).then(function (response) {
var deleted = response.toJSON();
deleted.tags.should.be.empty;
should.equal(deleted.author, undefined);
// Double check we can't find the post again
return PostModel.findOne(firstItemData);
}).then(function (newResults) {
should.equal(newResults, null);
done();
}).catch(done);
});
@ -372,6 +429,7 @@ describe('Post Model', function () {
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.posts.length.should.equal(30);
// Test both boolean formats
return PostModel.findPage({limit: 10, staticPages: true});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
@ -379,10 +437,26 @@ describe('Post Model', function () {
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.posts.length.should.equal(1);
// Test both boolean formats
return PostModel.findPage({limit: 10, staticPages: '1'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(10);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.posts.length.should.equal(1);
return PostModel.findPage({limit: 10, page: 2, status: 'all'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.pages.should.equal(11);
done();
}).catch(done);
});
it('can findPage for tag, with various options', function (done) {
testUtils.insertMorePosts().then(function () {
return testUtils.insertMorePostsTags();
}).then(function () {
// Test tag filter
return PostModel.findPage({page: 1, tag: 'bacon'});
}).then(function (paginationResult) {

View file

@ -79,19 +79,19 @@ describe('Role Model', function () {
}).catch(done);
});
it('can delete', function (done) {
RoleModel.findOne({id: 1}).then(function (foundRole) {
it('can destroy', function (done) {
var firstRole = {id: 1};
RoleModel.findOne(firstRole).then(function (foundRole) {
should.exist(foundRole);
foundRole.attributes.id.should.equal(firstRole.id);
return RoleModel.destroy(1);
}).then(function (destResp) {
return RoleModel.findAll();
}).then(function (foundRoles) {
var hasRemovedId = foundRoles.any(function (role) {
return role.id === 1;
});
hasRemovedId.should.equal(false);
return RoleModel.destroy(firstRole);
}).then(function (response) {
response.toJSON().should.be.empty;
return RoleModel.findOne(firstRole);
}).then(function (newResults) {
should.equal(newResults, null);
done();
}).catch(done);

View file

@ -153,39 +153,23 @@ describe('Settings Model', function () {
}).catch(done);
});
it('can delete', function (done) {
var settingId;
SettingsModel.findAll().then(function (results) {
it('can destroy', function (done) {
// dont't use id 1, since it will delete databaseversion
var settingToDestroy = {id: 2};
SettingsModel.findOne(settingToDestroy).then(function (results) {
should.exist(results);
results.attributes.id.should.equal(settingToDestroy.id);
results.length.should.be.above(0);
// dont't use results.models[0], since it will delete databaseversion
// which is used for testUtils.reset()
settingId = results.models[1].id;
return SettingsModel.destroy(settingId);
}).then(function () {
return SettingsModel.findAll();
return SettingsModel.destroy(settingToDestroy);
}).then(function (response) {
response.toJSON().should.be.empty;
return SettingsModel.findOne(settingToDestroy);
}).then(function (newResults) {
var ids, hasDeletedId;
ids = _.pluck(newResults.models, 'id');
hasDeletedId = _.any(ids, function (id) {
return id === settingId;
});
hasDeletedId.should.equal(false);
should.equal(newResults, null);
done();
}).catch(done);
});
});

View file

@ -144,7 +144,7 @@ describe('User Model', function run() {
it('sets last login time on successful login', function (done) {
var userData = testUtils.DataGenerator.forModel.users[0];
UserModel.check({email: userData.email, pw:userData.password}).then(function (activeUser) {
UserModel.check({email: userData.email, pw: userData.password}).then(function (activeUser) {
should.exist(activeUser.get('last_login'));
done();
}).catch(done);
@ -175,19 +175,13 @@ describe('User Model', function run() {
var firstUser;
UserModel.findAll().then(function (results) {
should.exist(results);
results.length.should.be.above(0);
firstUser = results.models[0];
return UserModel.findOne({email: firstUser.attributes.email});
}).then(function (found) {
should.exist(found);
found.attributes.name.should.equal(firstUser.attributes.name);
done();
@ -197,22 +191,18 @@ describe('User Model', function run() {
});
it('can edit', function (done) {
var firstUser;
UserModel.findAll().then(function (results) {
var firstUser = 1;
UserModel.findOne({id: firstUser}).then(function (results) {
var user;
should.exist(results);
user = results.toJSON();
user.id.should.equal(firstUser);
should.equal(user.website, null);
results.length.should.be.above(0);
firstUser = results.models[0];
return UserModel.edit({id: firstUser.id, website: "some.newurl.com"});
return UserModel.edit({website: 'some.newurl.com'}, {id: firstUser});
}).then(function (edited) {
should.exist(edited);
edited.attributes.website.should.equal('some.newurl.com');
done();
@ -220,41 +210,43 @@ describe('User Model', function run() {
}).catch(done);
});
it('can delete', function (done) {
var firstUserId;
it('can destroy', function (done) {
var firstUser = {id: 1};
UserModel.findAll().then(function (results) {
// Test that we have the user we expect
UserModel.findOne(firstUser).then(function (results) {
var user;
should.exist(results);
user = results.toJSON();
user.id.should.equal(firstUser.id);
results.length.should.be.above(0);
firstUserId = results.models[0].id;
return UserModel.destroy(firstUserId);
}).then(function () {
return UserModel.findAll();
// Destroy the user
return UserModel.destroy(firstUser);
}).then(function (response) {
response.toJSON().should.be.empty;
// Double check we can't find the user again
return UserModel.findOne(firstUser);
}).then(function (newResults) {
var ids, hasDeletedId;
should.equal(newResults, null);
if (newResults.length < 1) {
// Bug out if we only had one user and deleted it.
return done();
}
ids = _.pluck(newResults.models, "id");
hasDeletedId = _.any(ids, function (id) {
return id === firstUserId;
});
hasDeletedId.should.equal(false);
done();
}).catch(done);
});
});
describe('Password Reset', function () {
beforeEach(function (done) {
testUtils.initData()
.then(function () {
return when(testUtils.insertDefaultUser());
})
.then(function () {
done();
}).catch(done);
});
it('can generate reset token', function (done) {
// Expires in one minute

View file

@ -44,7 +44,7 @@ describe('Frontend Controller', function () {
});
apiSettingsStub = sandbox.stub(api.settings, 'read');
apiSettingsStub.withArgs('postsPerPage').returns(when({
apiSettingsStub.withArgs('postsPerPage').returns(when({
settings: [{
'key': 'postsPerPage',
'value': 6
@ -181,7 +181,7 @@ describe('Frontend Controller', function () {
done(new Error(msg));
};
};
beforeEach(function () {
sandbox.stub(api.posts, 'browse', function (args) {
return when({
@ -197,23 +197,23 @@ describe('Frontend Controller', function () {
}
});
});
apiSettingsStub = sandbox.stub(api.settings, 'read');
apiSettingsStub.withArgs('activeTheme').returns(when({
apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(when({
settings: [{
'key': 'activeTheme',
'value': 'casper'
}]
}));
apiSettingsStub.withArgs('postsPerPage').returns(when({
settings: [{
'key': 'postsPerPage',
'value': '10'
}]
}));
frontend.__set__('config', sandbox.stub().returns({
'paths': {
'subdir': '',
@ -229,15 +229,18 @@ describe('Frontend Controller', function () {
}
}));
});
describe('custom tag template', function () {
beforeEach(function () {
apiSettingsStub.withArgs('permalinks').returns(when({
value: '/tag/:slug/'
settings: [{
key: 'permalinks',
value: '/tag/:slug/'
}]
}));
});
it('it will render custom tag template if it exists', function (done) {
var req = {
path: '/tag/' + mockTags[0].slug,
@ -250,7 +253,7 @@ describe('Frontend Controller', function () {
done();
}
};
frontend.tag(req, res, failTest(done));
});
});
@ -422,7 +425,7 @@ describe('Frontend Controller', function () {
apiSettingsStub = sandbox.stub(api.settings, 'read');
apiSettingsStub.withArgs('activeTheme').returns(when({
apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(when({
settings: [{
'key': 'activeTheme',
'value': 'casper'
@ -451,7 +454,7 @@ describe('Frontend Controller', function () {
describe('custom page templates', function () {
beforeEach(function () {
apiSettingsStub.withArgs('permalinks').returns(when({
settings: [{
settings: [{
value: '/:slug/'
}]
}));
@ -547,8 +550,8 @@ describe('Frontend Controller', function () {
beforeEach(function () {
apiSettingsStub.withArgs('permalinks').returns(when({
settings: [{
value: '/:year/:month/:day/:slug/'
}]
value: '/:year/:month/:day/:slug/'
}]
}));
});
@ -621,7 +624,7 @@ describe('Frontend Controller', function () {
apiSettingsStub.withArgs('permalinks').returns(when({
settings: [{
value: '/:slug'
}]
}]
}));
});
@ -694,7 +697,7 @@ describe('Frontend Controller', function () {
apiSettingsStub.withArgs('permalinks').returns(when({
settings: [{
value: '/:year/:month/:day/:slug'
}]
}]
}));
});
@ -784,7 +787,7 @@ describe('Frontend Controller', function () {
apiSettingsStub.withArgs('permalinks').returns(when({
settings: [{
value: '/:year/:slug'
}]
}]
}));
});

View file

@ -24,16 +24,6 @@ describe('Permissions', function () {
}).catch(done);
});
beforeEach(function (done) {
sandbox = sinon.sandbox.create();
testUtils.initData()
.then(testUtils.insertDefaultUser)
.then(testUtils.insertDefaultApp)
.then(function () {
done();
}).catch(done);
});
afterEach(function (done) {
sandbox.restore();
testUtils.clearData()
@ -60,21 +50,7 @@ describe('Permissions', function () {
{ act: "remove", obj: "user" }
],
currTestPermId = 1,
// currTestUserId = 1,
// createTestUser = function (email) {
// if (!email) {
// currTestUserId += 1;
// email = "test" + currTestPermId + "@test.com";
// }
// var newUser = {
// id: currTestUserId,
// email: email,
// password: "testing123"
// };
// return UserProvider.add(newUser);
// },
createPermission = function (name, act, obj) {
if (!name) {
currTestPermId += 1;
@ -97,347 +73,374 @@ describe('Permissions', function () {
return when.all(createActions);
};
it('can load an actions map from existing permissions', function (done) {
describe('Init Permissions', function () {
createTestPermissions()
.then(permissions.init)
.then(function (actionsMap) {
should.exist(actionsMap);
beforeEach(function (done) {
sandbox = sinon.sandbox.create();
testUtils.initData()
.then(testUtils.insertDefaultUser)
.then(testUtils.insertDefaultApp)
.then(function () {
done();
}).catch(done);
});
actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page', 'theme', 'setting'].sort());
it('can load an actions map from existing permissions', function (done) {
createTestPermissions()
.then(permissions.init)
.then(function (actionsMap) {
should.exist(actionsMap);
actionsMap.should.equal(permissions.actionsMap);
actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page', 'theme', 'setting'].sort());
actionsMap.should.equal(permissions.actionsMap);
done();
}).catch(done);
});
it('can add user to role', function (done) {
var existingUserRoles;
UserProvider.findOne({id: 1}, { withRelated: ['roles'] }).then(function (foundUser) {
var testRole = new Models.Role({
name: 'testrole1',
description: 'testrole1 description'
});
should.exist(foundUser);
should.exist(foundUser.roles());
existingUserRoles = foundUser.related('roles').length;
return testRole.save(null, {user: 1}).then(function () {
return foundUser.roles().attach(testRole);
});
}).then(function () {
return UserProvider.findOne({id: 1}, { withRelated: ['roles'] });
}).then(function (updatedUser) {
should.exist(updatedUser);
updatedUser.related('roles').length.should.equal(existingUserRoles + 1);
done();
}).catch(done);
});
it('can add user to role', function (done) {
var existingUserRoles;
UserProvider.findOne({id: 1}, { withRelated: ['roles'] }).then(function (foundUser) {
var testRole = new Models.Role({
name: 'testrole1',
description: 'testrole1 description'
});
should.exist(foundUser);
should.exist(foundUser.roles());
existingUserRoles = foundUser.related('roles').length;
return testRole.save(null, {user: 1}).then(function () {
return foundUser.roles().attach(testRole);
});
}).then(function () {
return UserProvider.findOne({id: 1}, { withRelated: ['roles'] });
}).then(function (updatedUser) {
should.exist(updatedUser);
updatedUser.related('roles').length.should.equal(existingUserRoles + 1);
done();
}).catch(done);
});
it('can add user permissions', function (done) {
UserProvider.findOne({id: 1}, { withRelated: ['permissions']}).then(function (testUser) {
var testPermission = new Models.Permission({
name: "test edit posts",
action_type: 'edit',
object_type: 'post'
});
testUser.related('permissions').length.should.equal(0);
return testPermission.save(null, {user: 1}).then(function () {
return testUser.permissions().attach(testPermission);
});
}).then(function () {
return UserProvider.findOne({id: 1}, { withRelated: ['permissions']});
}).then(function (updatedUser) {
should.exist(updatedUser);
updatedUser.related('permissions').length.should.equal(1);
done();
}).catch(done);
});
it('can add role permissions', function (done) {
var testRole = new Models.Role({
name: "test2",
description: "test2 description"
});
testRole.save(null, {user: 1})
.then(function () {
return testRole.load('permissions');
})
.then(function () {
var rolePermission = new Models.Permission({
it('can add user permissions', function (done) {
UserProvider.findOne({id: 1}, { withRelated: ['permissions']}).then(function (testUser) {
var testPermission = new Models.Permission({
name: "test edit posts",
action_type: 'edit',
object_type: 'post'
});
testRole.related('permissions').length.should.equal(0);
testUser.related('permissions').length.should.equal(0);
return rolePermission.save(null, {user: 1}).then(function () {
return testRole.permissions().attach(rolePermission);
return testPermission.save(null, {user: 1}).then(function () {
return testUser.permissions().attach(testPermission);
});
})
.then(function () {
return Models.Role.findOne({id: testRole.id}, { withRelated: ['permissions']});
})
.then(function (updatedRole) {
should.exist(updatedRole);
updatedRole.related('permissions').length.should.equal(1);
done();
}).catch(done);
});
it('does not allow edit post without permission', function (done) {
var fakePage = {
id: 1
};
createTestPermissions()
.then(permissions.init)
.then(function () {
return UserProvider.findOne({id: 1});
})
.then(function (foundUser) {
var canThisResult = permissions.canThis(foundUser);
should.exist(canThisResult.edit);
should.exist(canThisResult.edit.post);
return canThisResult.edit.page(fakePage);
})
.then(function () {
errors.logError(new Error("Allowed edit post without permission"));
}).catch(done);
});
it('allows edit post with permission', function (done) {
var fakePost = {
id: "1"
};
createTestPermissions()
.then(permissions.init)
.then(function () {
return UserProvider.findOne({id: 1});
})
.then(function (foundUser) {
var newPerm = new Models.Permission({
name: "test3 edit post",
action_type: "edit",
object_type: "post"
});
return newPerm.save(null, {user: 1}).then(function () {
return foundUser.permissions().attach(newPerm);
});
})
.then(function () {
}).then(function () {
return UserProvider.findOne({id: 1}, { withRelated: ['permissions']});
})
.then(function (updatedUser) {
}).then(function (updatedUser) {
should.exist(updatedUser);
// TODO: Verify updatedUser.related('permissions') has the permission?
var canThisResult = permissions.canThis(updatedUser.id);
updatedUser.related('permissions').length.should.equal(1);
should.exist(canThisResult.edit);
should.exist(canThisResult.edit.post);
return canThisResult.edit.post(fakePost);
})
.then(function () {
done();
}).catch(done);
});
});
it('can use permissable function on Model to allow something', function (done) {
var testUser,
permissableStub = sandbox.stub(PostProvider, 'permissable', function () {
return when.resolve();
it('can add role permissions', function (done) {
var testRole = new Models.Role({
name: "test2",
description: "test2 description"
});
testUtils.insertAuthorUser()
.then(function () {
return UserProvider.findAll();
})
.then(function (foundUser) {
testUser = foundUser.models[1];
testRole.save(null, {user: 1})
.then(function () {
return testRole.load('permissions');
})
.then(function () {
var rolePermission = new Models.Permission({
name: "test edit posts",
action_type: 'edit',
object_type: 'post'
});
return permissions.canThis(testUser).edit.post(123);
})
.then(function () {
permissableStub.restore();
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true);
testRole.related('permissions').length.should.equal(0);
done();
})
.catch(function () {
permissableStub.restore();
errors.logError(new Error("Did not allow testUser"));
return rolePermission.save(null, {user: 1}).then(function () {
return testRole.permissions().attach(rolePermission);
});
})
.then(function () {
return Models.Role.findOne({id: testRole.id}, { withRelated: ['permissions']});
})
.then(function (updatedRole) {
should.exist(updatedRole);
updatedRole.related('permissions').length.should.equal(1);
done();
}).catch(done);
});
done();
});
});
it('can use permissable function on Model to forbid something', function (done) {
var testUser,
permissableStub = sandbox.stub(PostProvider, 'permissable', function () {
return when.reject();
});
describe('With Permissions', function () {
testUtils.insertAuthorUser()
.then(function () {
return UserProvider.findAll();
})
.then(function (foundUser) {
testUser = foundUser.models[1];
beforeEach(function (done) {
sandbox = sinon.sandbox.create();
testUtils.initData()
.then(testUtils.insertDefaultUser)
.then(testUtils.insertDefaultApp)
.then(function () {
done();
}).catch(done);
});
return permissions.canThis(testUser).edit.post(123);
})
.then(function () {
permissableStub.restore();
done(new Error("Allowed testUser to edit post"));
})
.catch(function () {
permissableStub.restore();
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true);
done();
});
});
it('does not allow edit post without permission', function (done) {
var fakePage = {
id: 1
};
it("can get effective user permissions", function (done) {
effectivePerms.user(1).then(function (effectivePermissions) {
should.exist(effectivePermissions);
createTestPermissions()
.then(permissions.init)
.then(function () {
return UserProvider.findOne({id: 1});
})
.then(function (foundUser) {
var canThisResult = permissions.canThis(foundUser);
effectivePermissions.length.should.be.above(0);
should.exist(canThisResult.edit);
should.exist(canThisResult.edit.post);
done();
}).catch(done);
});
return canThisResult.edit.page(fakePage);
})
.then(function () {
errors.logError(new Error("Allowed edit post without permission"));
}).catch(done);
});
it('can check an apps effective permissions', function (done) {
effectivePerms.app('Kudos')
.then(function (effectivePermissions) {
it('allows edit post with permission', function (done) {
var fakePost = {
id: "1"
};
createTestPermissions()
.then(permissions.init)
.then(function () {
return UserProvider.findOne({id: 1});
})
.then(function (foundUser) {
var newPerm = new Models.Permission({
name: "test3 edit post",
action_type: "edit",
object_type: "post"
});
return newPerm.save(null, {user: 1}).then(function () {
return foundUser.permissions().attach(newPerm);
});
})
.then(function () {
return UserProvider.findOne({id: 1}, { withRelated: ['permissions']});
})
.then(function (updatedUser) {
// TODO: Verify updatedUser.related('permissions') has the permission?
var canThisResult = permissions.canThis(updatedUser.id);
should.exist(canThisResult.edit);
should.exist(canThisResult.edit.post);
return canThisResult.edit.post(fakePost);
})
.then(function () {
done();
}).catch(done);
});
it('can use permissable function on Model to allow something', function (done) {
var testUser,
permissableStub = sandbox.stub(PostProvider, 'permissable', function () {
return when.resolve();
});
testUtils.insertAuthorUser()
.then(function () {
return UserProvider.findAll();
})
.then(function (foundUser) {
testUser = foundUser.models[1];
return permissions.canThis({user: testUser.id}).edit.post(123);
})
.then(function () {
permissableStub.restore();
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true);
done();
})
.catch(function () {
permissableStub.restore();
errors.logError(new Error("Did not allow testUser"));
done();
});
});
it('can use permissable function on Model to forbid something', function (done) {
var testUser,
permissableStub = sandbox.stub(PostProvider, 'permissable', function () {
return when.reject();
});
testUtils.insertAuthorUser()
.then(function () {
return UserProvider.findAll();
})
.then(function (foundUser) {
testUser = foundUser.models[1];
return permissions.canThis({user: testUser.id}).edit.post(123);
})
.then(function () {
permissableStub.restore();
done(new Error("Allowed testUser to edit post"));
})
.catch(function () {
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true);
permissableStub.restore();
done();
});
});
it("can get effective user permissions", function (done) {
effectivePerms.user(1).then(function (effectivePermissions) {
should.exist(effectivePermissions);
effectivePermissions.length.should.be.above(0);
done();
})
.catch(done);
});
}).catch(done);
});
it('does not allow an app to edit a post without permission', function (done) {
// Change the author of the post so the author override doesn't affect the test
PostProvider.edit({id: 1, 'author_id': 2})
.then(function (updatedPost) {
// Add user permissions
return UserProvider.findOne({id: 1})
.then(function (foundUser) {
var newPerm = new Models.Permission({
name: "app test edit post",
action_type: "edit",
object_type: "post"
});
it('can check an apps effective permissions', function (done) {
effectivePerms.app('Kudos')
.then(function (effectivePermissions) {
should.exist(effectivePermissions);
return newPerm.save(null, {user: 1}).then(function () {
return foundUser.permissions().attach(newPerm).then(function () {
return when.all([updatedPost, foundUser]);
effectivePermissions.length.should.be.above(0);
done();
})
.catch(done);
});
it('does not allow an app to edit a post without permission', function (done) {
// Change the author of the post so the author override doesn't affect the test
PostProvider.edit({'author_id': 2}, {id: 1})
.then(function (updatedPost) {
// Add user permissions
return UserProvider.findOne({id: 1})
.then(function (foundUser) {
var newPerm = new Models.Permission({
name: "app test edit post",
action_type: "edit",
object_type: "post"
});
return newPerm.save(null, {user: 1}).then(function () {
return foundUser.permissions().attach(newPerm).then(function () {
return when.all([updatedPost, foundUser]);
});
});
});
});
})
.then(function (results) {
var updatedPost = results[0],
updatedUser = results[1];
})
.then(function (results) {
var updatedPost = results[0],
updatedUser = results[1];
return permissions.canThis({ user: updatedUser.id })
.edit
.post(updatedPost.id)
.then(function () {
return results;
})
.catch(function (err) {
done(new Error("Did not allow user 1 to edit post 1"));
});
})
.then(function (results) {
var updatedPost = results[0],
updatedUser = results[1];
return permissions.canThis({ user: updatedUser.id })
.edit
.post(updatedPost.id)
.then(function () {
return results;
})
.catch(function (err) {
done(new Error("Did not allow user 1 to edit post 1"));
});
})
.then(function (results) {
var updatedPost = results[0],
updatedUser = results[1];
// Confirm app cannot edit it.
return permissions.canThis({ app: 'Hemingway', user: updatedUser.id })
.edit
.post(updatedPost.id)
.then(function () {
done(new Error("Allowed an edit of post 1"));
})
.catch(function () {
done();
});
}).catch(done);
});
// Confirm app cannot edit it.
return permissions.canThis({ app: 'Hemingway', user: updatedUser.id })
.edit
.post(updatedPost.id)
.then(function () {
done(new Error("Allowed an edit of post 1"));
})
.catch(function () {
done();
});
}).catch(done);
});
it('allows an app to edit a post with permission', function (done) {
permissions.canThis({ app: 'Kudos', user: 1 })
.edit
.post(1)
.then(function () {
done();
})
.catch(function () {
done(new Error("Allowed an edit of post 1"));
});
});
it('allows an app to edit a post with permission', function (done) {
permissions.canThis({ app: 'Kudos', user: 1 })
.edit
.post(1)
.then(function () {
done();
})
.catch(function () {
done(new Error("Allowed an edit of post 1"));
});
});
it('checks for null context passed and rejects', function (done) {
permissions.canThis(undefined)
.edit
.post(1)
.then(function () {
done(new Error("Should not allow editing post"));
})
.catch(function () {
done();
});
});
it('checks for null context passed and rejects', function (done) {
permissions.canThis(undefined)
.edit
.post(1)
.then(function () {
done(new Error("Should not allow editing post"));
})
.catch(function () {
done();
});
});
it('allows \'internal\' to be passed for internal requests', function (done) {
// Using tag here because post implements the custom permissable interface
permissions.canThis('internal')
.edit
.tag(1)
.then(function () {
done();
})
.catch(function () {
done(new Error("Should allow editing post with 'internal'"));
});
});
it('allows \'internal\' to be passed for internal requests', function (done) {
// Using tag here because post implements the custom permissable interface
permissions.canThis('internal')
.edit
.tag(1)
.then(function () {
done();
})
.catch(function () {
done(new Error("Should allow editing post with 'internal'"));
});
});
it('allows { internal: true } to be passed for internal requests', function (done) {
// Using tag here because post implements the custom permissable interface
permissions.canThis({ internal: true })
.edit
.tag(1)
.then(function () {
done();
})
.catch(function () {
done(new Error("Should allow editing post with { internal: true }"));
});
it('allows { internal: true } to be passed for internal requests', function (done) {
// Using tag here because post implements the custom permissable interface
permissions.canThis({ internal: true })
.edit
.tag(1)
.then(function () {
done();
})
.catch(function () {
done(new Error("Should allow editing post with { internal: true }"));
});
});
});
});

View file

@ -10,7 +10,7 @@ var url = require('url'),
post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description',
'featured', 'image', 'status', 'language', 'created_at', 'created_by', 'updated_at',
'updated_by', 'published_at', 'published_by', 'page', 'author', 'tags', 'fields'],
settings: ['settings'],
settings: ['settings', 'meta'],
setting: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'updated_by'],
tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent',
'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'],