Sorted out the mixed usages of include and withRelated (#9425)

no issue

- this commit cleans up the usages of `include` and `withRelated`.

### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)

### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf

---

Commits explained.

* Reorder the usage of `convertOptions`

- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
  - we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)

* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer

* Change `convertOptions` API utiliy

- API Usage
  - ghost.api(..., {include: 'tags,authors'})
  - `include` should only be used when calling the API (either via request or via manual usage)
  - `include` is only for readability and easier format
- Ghost (Model Layer Usage)
  - models.Post.findOne(..., {withRelated: ['tags', 'authors']})
  - should only use `withRelated`
  - model layer cannot read 'tags,authors`
  - model layer has no idea what `include` means, speaks a different language
  - `withRelated` is bookshelf
  - internal usage

* include-count plugin: use `withRelated` instead of `include`

- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf

* Updated `permittedOptions` in base model

- `include` is no longer a known option

* Remove all occurances of `include` in the model layer

* Extend `filterOptions` base function

- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit

* Ensure we call `filterOptions` as first action

- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
  - if you override a function e.g. `edit`
  - then you should call `filterOptions` as first action
  - the base implementation of e.g. `edit` will call it again
  - future improvement

* Removed `findOne` from Invite model

- no longer needed, the base implementation is the same
This commit is contained in:
Katharina Irrgang 2018-02-15 10:53:53 +01:00 committed by GitHub
parent d87fe38019
commit c6a95c6478
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 254 additions and 277 deletions

View File

@ -52,6 +52,7 @@ clients = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {attrs: attrs}),
localUtils.convertOptions(),
// TODO: add permissions
// utils.handlePublicPermissions(docName, 'read'),
doQuery

View File

@ -23,8 +23,8 @@ invites = {
tasks = [
localUtils.validate(docName, {opts: localUtils.browseDefaultOptions}),
localUtils.handlePublicPermissions(docName, 'browse'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePublicPermissions(docName, 'browse'),
modelQuery
];
@ -52,8 +52,8 @@ invites = {
tasks = [
localUtils.validate(docName, {attrs: attrs}),
localUtils.handlePublicPermissions(docName, 'read'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePublicPermissions(docName, 'read'),
modelQuery
];
@ -76,8 +76,8 @@ invites = {
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
localUtils.handlePermissions(docName, 'destroy'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'destroy'),
modelQuery
];
@ -221,7 +221,7 @@ invites = {
}
function fetchLoggedInUser(options) {
return models.User.findOne({id: loggedInUser}, _.merge({}, _.omit(options, 'data'), {include: ['roles']}))
return models.User.findOne({id: loggedInUser}, _.merge({}, _.omit(options, 'data'), {withRelated: ['roles']}))
.then(function (user) {
if (!user) {
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.users.userNotFound')}));
@ -234,8 +234,8 @@ invites = {
tasks = [
localUtils.validate(docName, {opts: ['email']}),
localUtils.handlePermissions(docName, 'add'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'add'),
fetchLoggedInUser,
validation,
checkIfUserExists,

View File

@ -59,8 +59,8 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: permittedOptions}),
localUtils.handlePublicPermissions(docName, 'browse', unsafeAttrs),
localUtils.convertOptions(allowedIncludes, models.Post.allowedFormats),
localUtils.handlePublicPermissions(docName, 'browse', unsafeAttrs),
modelQuery
];
@ -106,8 +106,8 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {attrs: attrs, opts: extraAllowedOptions}),
localUtils.handlePublicPermissions(docName, 'read', unsafeAttrs),
localUtils.convertOptions(allowedIncludes, models.Post.allowedFormats),
localUtils.handlePublicPermissions(docName, 'read', unsafeAttrs),
modelQuery
];
@ -162,8 +162,8 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions.concat(extraAllowedOptions)}),
localUtils.handlePermissions(docName, 'edit', unsafeAttrs),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'edit', unsafeAttrs),
modelQuery
];
@ -206,8 +206,8 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName),
localUtils.handlePermissions(docName, 'add', unsafeAttrs),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'add', unsafeAttrs),
modelQuery
];
@ -245,8 +245,8 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
localUtils.handlePermissions(docName, 'destroy', unsafeAttrs),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'destroy', unsafeAttrs),
deletePost
];

View File

@ -66,6 +66,7 @@ roles = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: permittedOptions}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'browse'),
modelQuery
];

View File

@ -73,6 +73,7 @@ slugs = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: opts, attrs: attrs}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'generate'),
checkAllowedTypes,
modelQuery

View File

@ -38,6 +38,7 @@ subscribers = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.browseDefaultOptions}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'browse'),
doQuery
];
@ -79,6 +80,7 @@ subscribers = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {attrs: attrs}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'read'),
doQuery
];
@ -129,6 +131,7 @@ subscribers = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'add'),
doQuery
];
@ -171,6 +174,7 @@ subscribers = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'edit'),
doQuery
];
@ -224,6 +228,7 @@ subscribers = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: ['id', 'email']}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'destroy'),
getSubscriberByEmail,
doQuery
@ -279,6 +284,7 @@ subscribers = {
}
tasks = [
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'browse'),
exportSubscribers
];
@ -341,6 +347,7 @@ subscribers = {
}
tasks = [
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'add'),
importCSV
];

View File

@ -37,8 +37,8 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.browseDefaultOptions}),
localUtils.handlePublicPermissions(docName, 'browse'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePublicPermissions(docName, 'browse'),
doQuery
];
@ -79,8 +79,8 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {attrs: attrs}),
localUtils.handlePublicPermissions(docName, 'read'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePublicPermissions(docName, 'read'),
doQuery
];
@ -114,8 +114,8 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName),
localUtils.handlePermissions(docName, 'add'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'add'),
doQuery
];
@ -157,8 +157,8 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
localUtils.handlePermissions(docName, 'edit'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'edit'),
doQuery
];
@ -188,8 +188,8 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
localUtils.handlePermissions(docName, 'destroy'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePermissions(docName, 'destroy'),
deleteTag
];

View File

@ -42,8 +42,8 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: permittedOptions}),
localUtils.handlePublicPermissions(docName, 'browse'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePublicPermissions(docName, 'browse'),
doQuery
];
@ -89,8 +89,8 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {attrs: attrs}),
localUtils.handlePublicPermissions(docName, 'read'),
localUtils.convertOptions(allowedIncludes),
localUtils.handlePublicPermissions(docName, 'read'),
doQuery
];
@ -153,7 +153,7 @@ users = {
editedUserId = options.id;
return models.User.findOne(
{id: options.context.user, status: 'all'}, {include: ['roles']}
{id: options.context.user, status: 'all'}, {withRelated: ['roles']}
).then(function (contextUser) {
var contextRoleId = contextUser.related('roles').toJSON(options)[0].id;
@ -213,8 +213,8 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: permittedOptions}),
handlePermissions,
localUtils.convertOptions(allowedIncludes),
handlePermissions,
doQuery
];
@ -273,8 +273,8 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
handlePermissions,
localUtils.convertOptions(allowedIncludes),
handlePermissions,
deleteUser
];
@ -343,8 +343,8 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
validateRequest,
handlePermissions,
localUtils.convertOptions(allowedIncludes),
handlePermissions,
doQuery
];
@ -395,8 +395,8 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate('owner'),
handlePermissions,
localUtils.convertOptions(allowedIncludes),
handlePermissions,
doQuery
];

View File

@ -277,7 +277,8 @@ utils = {
*/
return function doConversion(options) {
if (options.include) {
options.include = utils.prepareInclude(options.include, allowedIncludes);
options.withRelated = utils.prepareInclude(options.include, allowedIncludes);
delete options.include;
}
if (options.fields) {

View File

@ -92,6 +92,7 @@ webhooks = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'add'),
doQuery
];
@ -122,6 +123,7 @@ webhooks = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
localUtils.convertOptions(),
localUtils.handlePermissions(docName, 'destroy'),
doQuery
];

View File

@ -322,14 +322,14 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
*
* `toJSON` calls `serialize`.
*
* @param options
* @param unfilteredOptions
* @returns {*}
*/
toJSON: function toJSON(options) {
var opts = _.cloneDeep(options || {});
opts.omitPivot = true;
toJSON: function toJSON(unfilteredOptions) {
var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'toJSON');
options.omitPivot = true;
return proto.toJSON.call(this, opts);
return proto.toJSON.call(this, options);
},
// Get attributes that have been updated (values before a .save() call)
@ -389,9 +389,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
*
* @return {Object} Keys allowed in the `options` hash of every model's method.
*/
permittedOptions: function permittedOptions() {
permittedOptions: function permittedOptions(methodName) {
if (methodName === 'toJSON') {
return ['shallow', 'withRelated', 'context', 'columns'];
}
// terms to whitelist for all methods.
return ['context', 'include', 'transacting', 'importing', 'forUpdate'];
return ['context', 'withRelated', 'transacting', 'importing', 'forUpdate'];
},
/**
@ -455,15 +459,29 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
/**
* Filters potentially unsafe `options` in a model method's arguments, so you can pass them to Bookshelf / Knex.
* @param {Object} options Represents options to filter in order to be passed to the Bookshelf query.
* @param {Object} unfilteredOptions Represents options to filter in order to be passed to the Bookshelf query.
* @param {String} methodName The name of the method to check valid options for.
* @return {Object} The filtered results of `options`.
*/
filterOptions: function filterOptions(options, methodName) {
var permittedOptions = this.permittedOptions(methodName, options),
filteredOptions = _.pick(options, permittedOptions);
filterOptions: function filterOptions(unfilteredOptions, methodName, filterConfig) {
unfilteredOptions = unfilteredOptions || {};
filterConfig = filterConfig || {};
return filteredOptions;
if (unfilteredOptions.hasOwnProperty('include')) {
throw new common.errors.IncorrectUsageError({
message: 'The model layer expects using `withRelated`.'
});
}
var options = _.cloneDeep(unfilteredOptions),
extraAllowedProperties = filterConfig.extraAllowedProperties || [],
permittedOptions;
permittedOptions = this.permittedOptions(methodName, options);
permittedOptions = _.union(permittedOptions, extraAllowedProperties);
options = _.pick(options, permittedOptions);
return options;
},
// ## Model Data Functions
@ -471,14 +489,12 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
/**
* ### Find All
* Fetches all the data for a particular model
* @param {Object} options (optional)
* @param {Object} unfilteredOptions (optional)
* @return {Promise(ghostBookshelf.Collection)} Collection of all Models
*/
findAll: function findAll(options) {
options = this.filterOptions(options, 'findAll');
options.withRelated = _.union(options.withRelated, options.include);
var itemCollection = this.forge();
findAll: function findAll(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'findAll'),
itemCollection = this.forge();
// transforms fictive keywords like 'all' (status:all) into correct allowed values
if (this.processOptions) {
@ -488,9 +504,9 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
itemCollection.applyDefaultAndCustomFilters(options);
return itemCollection.fetchAll(options).then(function then(result) {
if (options.include) {
if (options.withRelated) {
_.each(result.models, function each(item) {
item.include = options.include;
item.withRelated = options.withRelated;
});
}
@ -516,12 +532,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
* total: __
* }
*
* @param {Object} options
* @param {Object} unfilteredOptions
*/
findPage: function findPage(options) {
options = options || {};
var self = this,
findPage: function findPage(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'findPage'),
itemCollection = this.forge(),
tableName = _.result(this.prototype, 'tableName'),
requestedColumns = options.columns;
@ -529,9 +543,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// Set this to true or pass ?debug=true as an API option to get output
itemCollection.debug = options.debug && config.get('env') !== 'production';
// Filter options so that only permitted ones remain
options = this.filterOptions(options, 'findPage');
// This applies default properties like 'staticPages' and 'status'
// And then converts them to 'where' options... this behaviour is effectively deprecated in favour
// of using filter - it's only be being kept here so that we can transition cleanly.
@ -540,10 +551,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// Add Filter behaviour
itemCollection.applyDefaultAndCustomFilters(options);
// Handle related objects
// TODO: this should just be done for all methods @ the API level
options.withRelated = _.union(options.withRelated, options.include);
// Ensure only valid fields/columns are added to query
// and append default columns to fetch
if (options.columns) {
@ -552,16 +559,16 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
}
if (options.order) {
options.order = self.parseOrderOption(options.order, options.include);
} else if (self.orderDefaultRaw) {
options.orderRaw = self.orderDefaultRaw();
options.order = this.parseOrderOption(options.order, options.withRelated);
} else if (this.orderDefaultRaw) {
options.orderRaw = this.orderDefaultRaw();
} else {
options.order = self.orderDefaultOptions();
options.order = this.orderDefaultOptions();
}
return itemCollection.fetchPage(options).then(function formatResponse(response) {
var data = {},
models = [];
models;
options.columns = requestedColumns;
models = response.collection.toJSON(options);
@ -571,6 +578,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
data[tableName] = _.map(models, function transform(model) {
return options.columns ? _.pick(model, options.columns) : model;
});
data.meta = {pagination: response.pagination};
return data;
});
@ -580,13 +588,12 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
* ### Find One
* Naive find one where data determines what to match on
* @param {Object} data
* @param {Object} options (optional)
* @param {Object} unfilteredOptions (optional)
* @return {Promise(ghostBookshelf.Model)} Single Model
*/
findOne: function findOne(data, options) {
findOne: function findOne(data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'findOne');
data = this.filterData(data);
options = this.filterOptions(options, 'findOne');
return this.forge(data).fetch(options);
},
@ -598,15 +605,15 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
* Based on the `method` option Bookshelf and Ghost can determine if a query is an insert or an update.
*
* @param {Object} data
* @param {Object} options (optional)
* @param {Object} unfilteredOptions (optional)
* @return {Promise(ghostBookshelf.Model)} Edited Model
*/
edit: function edit(data, options) {
var id = options.id,
edit: function edit(data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'edit', {extraAllowedProperties: ['id']}),
id = options.id,
model = this.forge({id: id});
data = this.filterData(data);
options = this.filterOptions(options, 'edit');
// We allow you to disable timestamps when run migration, so that the posts `updated_at` value is the same
if (options.importing) {
@ -624,13 +631,15 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
* ### Add
* Naive add
* @param {Object} data
* @param {Object} options (optional)
* @param {Object} unfilteredOptions (optional)
* @return {Promise(ghostBookshelf.Model)} Newly Added Model
*/
add: function add(data, options) {
add: function add(data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'add'),
model;
data = this.filterData(data);
options = this.filterOptions(options, 'add');
var model = this.forge(data);
model = 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
@ -647,15 +656,17 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
/**
* ### Destroy
* Naive destroy
* @param {Object} options (optional)
* @param {Object} unfilteredOptions (optional)
* @return {Promise(ghostBookshelf.Model)} Empty Model
*/
destroy: function destroy(options) {
var id = options.id;
options = this.filterOptions(options, 'destroy');
destroy: function destroy(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']}),
id = options.id;
// Fetch the object before destroying it, so that the changed data is available to events
return this.forge({id: id}).fetch(options).then(function then(obj) {
return this.forge({id: id})
.fetch(options)
.then(function then(obj) {
return obj.destroy(options);
});
},
@ -757,11 +768,11 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
});
},
parseOrderOption: function (order, include) {
parseOrderOption: function (order, withRelated) {
var permittedAttributes, result, rules;
permittedAttributes = this.prototype.permittedAttributes();
if (include && include.indexOf('count.posts') > -1) {
if (withRelated && withRelated.indexOf('count.posts') > -1) {
permittedAttributes.push('count.posts');
}
result = {};

View File

@ -21,8 +21,9 @@ Basetoken = ghostBookshelf.Model.extend({
}
}, {
destroyAllExpired: function destroyAllExpired(options) {
options = this.filterOptions(options, 'destroyAll');
destroyAllExpired: function destroyAllExpired(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'destroyAll');
return ghostBookshelf.Collection.forge([], {model: this})
.query('where', 'expires', '<', Date.now())
.fetch(options)
@ -33,12 +34,11 @@ Basetoken = ghostBookshelf.Model.extend({
/**
* ### destroyByUser
* @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy
* @param {[type]} unfilteredOptions has context and id. Context is the user doing the destroy, id is the user to destroy
*/
destroyByUser: function destroyByUser(options) {
var userId = options.id;
options = this.filterOptions(options, 'destroyByUser');
destroyByUser: function destroyByUser(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'destroyByUser', {extraAllowedProperties: ['id']}),
userId = options.id;
if (userId) {
return ghostBookshelf.Collection.forge([], {model: this})
@ -54,12 +54,12 @@ Basetoken = ghostBookshelf.Model.extend({
/**
* ### destroyByToken
* @param {[type]} options has token where token is the token to destroy
* @param {[type]} unfilteredOptions has token where token is the token to destroy
*/
destroyByToken: function destroyByToken(options) {
var token = options.token;
destroyByToken: function destroyByToken(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'destroyByToken', {extraAllowedProperties: ['token']}),
token = options.token;
options = this.filterOptions(options, 'destroyByUser');
options.require = true;
return this.forge()

View File

@ -1,7 +1,6 @@
'use strict';
const crypto = require('crypto'),
_ = require('lodash'),
constants = require('../lib/constants'),
ghostBookshelf = require('./base');
@ -11,10 +10,10 @@ let Invite,
Invite = ghostBookshelf.Model.extend({
tableName: 'invites',
toJSON: function (options) {
options = options || {};
toJSON: function (unfilteredOptions) {
var options = Invite.filterOptions(unfilteredOptions, 'toJSON'),
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
delete attrs.token;
return attrs;
}
@ -27,31 +26,10 @@ Invite = ghostBookshelf.Model.extend({
return options;
},
/**
* @TODO: can't use base class, because:
* options.withRelated = _.union(options.withRelated, options.include); is missing
* there are some weird self implementations in each model
* so adding this line, will destroy other models, because they rely on something else
* FIX ME!!!!!
*/
findOne: function findOne(data, options) {
options = options || {};
options = this.filterOptions(options, 'findOne');
data = this.filterData(data, 'findOne');
options.withRelated = _.union(options.withRelated, options.include);
var invite = this.forge(data);
return invite.fetch(options);
},
add: function add(data, options) {
var hash = crypto.createHash('sha256'),
text = '';
options = this.filterOptions(options, 'add');
options.withRelated = _.union(options.withRelated, options.include);
data.expires = Date.now() + constants.ONE_WEEK_MS;
data.status = 'pending';
@ -60,6 +38,7 @@ Invite = ghostBookshelf.Model.extend({
hash.update(data.email.toLocaleLowerCase());
text += [data.expires, data.email, hash.digest('base64')].join('|');
data.token = new Buffer(text).toString('base64');
return ghostBookshelf.Model.add.call(this, data, options);
}
});

View File

@ -51,8 +51,8 @@ module.exports = function (Bookshelf) {
var tableName = _.result(this, 'tableName');
if (options.include && options.include.indexOf('count.posts') > -1) {
// remove post_count from withRelated and include
if (options.withRelated && options.withRelated.indexOf('count.posts') > -1) {
// remove post_count from withRelated
options.withRelated = _.pull([].concat(options.withRelated), 'count.posts');
// Call the query builder

View File

@ -350,10 +350,9 @@ Post = ghostBookshelf.Model.extend({
return attrs;
},
toJSON: function toJSON(options) {
options = options || {};
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options),
toJSON: function toJSON(unfilteredOptions) {
var options = Post.filterOptions(unfilteredOptions, 'toJSON'),
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options),
oldPostId = attrs.amp,
commentId;
@ -520,8 +519,6 @@ Post = ghostBookshelf.Model.extend({
* **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
*/
findOne: function findOne(data, options) {
options = options || {};
data = _.defaults(data || {}, {
status: 'published'
});
@ -530,8 +527,6 @@ Post = ghostBookshelf.Model.extend({
delete data.status;
}
options.withRelated = _.union(options.withRelated, options.include);
return ghostBookshelf.Model.findOne.call(this, data, options);
},
@ -542,15 +537,15 @@ Post = ghostBookshelf.Model.extend({
* @extends ghostBookshelf.Model.edit to handle returning the full object and manage _updatedAttributes
* **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
*/
edit: function edit(data, options) {
let opts = _.cloneDeep(options || {});
edit: function edit(data, unfilteredOptions) {
let options = this.filterOptions(unfilteredOptions, 'edit', {extraAllowedProperties: ['id']});
const editPost = () => {
opts.forUpdate = true;
options.forUpdate = true;
return ghostBookshelf.Model.edit.call(this, data, opts)
return ghostBookshelf.Model.edit.call(this, data, options)
.then((post) => {
return this.findOne({status: 'all', id: opts.id}, opts)
return this.findOne({status: 'all', id: options.id}, options)
.then((found) => {
if (found) {
// Pass along the updated attributes for checking status changes
@ -561,9 +556,9 @@ Post = ghostBookshelf.Model.extend({
});
};
if (!opts.transacting) {
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
opts.transacting = transacting;
options.transacting = transacting;
return editPost();
});
}
@ -576,19 +571,19 @@ Post = ghostBookshelf.Model.extend({
* @extends ghostBookshelf.Model.add to handle returning the full object
* **See:** [ghostBookshelf.Model.add](base.js.html#add)
*/
add: function add(data, options) {
let opts = _.cloneDeep(options || {});
add: function add(data, unfilteredOptions) {
let options = this.filterOptions(unfilteredOptions, 'add', {extraAllowedProperties: ['id']});
const addPost = (() => {
return ghostBookshelf.Model.add.call(this, data, opts)
return ghostBookshelf.Model.add.call(this, data, options)
.then((post) => {
return this.findOne({status: 'all', id: post.id}, opts);
return this.findOne({status: 'all', id: post.id}, options);
});
});
if (!opts.transacting) {
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
opts.transacting = transacting;
options.transacting = transacting;
return addPost();
});
@ -597,16 +592,16 @@ Post = ghostBookshelf.Model.extend({
return addPost();
},
destroy: function destroy(options) {
let opts = _.cloneDeep(options || {});
destroy: function destroy(unfilteredOptions) {
let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
const destroyPost = () => {
return ghostBookshelf.Model.destroy.call(this, opts);
return ghostBookshelf.Model.destroy.call(this, options);
};
if (!opts.transacting) {
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
opts.transacting = transacting;
options.transacting = transacting;
return destroyPost();
});
}
@ -618,13 +613,10 @@ Post = ghostBookshelf.Model.extend({
* ### destroyByAuthor
* @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy
*/
destroyByAuthor: function destroyByAuthor(options) {
let opts = _.cloneDeep(options || {});
let postCollection = Posts.forge(),
authorId = opts.id;
opts = this.filterOptions(opts, 'destroyByAuthor');
destroyByAuthor: function destroyByAuthor(unfilteredOptions) {
let options = this.filterOptions(unfilteredOptions, 'destroyByAuthor', {extraAllowedProperties: ['id']}),
postCollection = Posts.forge(),
authorId = options.id;
if (!authorId) {
throw new common.errors.NotFoundError({
@ -635,16 +627,16 @@ Post = ghostBookshelf.Model.extend({
const destroyPost = (() => {
return postCollection
.query('where', 'author_id', '=', authorId)
.fetch(opts)
.call('invokeThen', 'destroy', opts)
.fetch(options)
.call('invokeThen', 'destroy', options)
.catch((err) => {
throw new common.errors.GhostError({err: err});
});
});
if (!opts.transacting) {
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
opts.transacting = transacting;
options.transacting = transacting;
return destroyPost();
});
}

View File

@ -98,9 +98,9 @@ Settings = ghostBookshelf.Model.extend({
return Promise.resolve(ghostBookshelf.Model.findOne.call(this, data, options));
},
edit: function (data, options) {
var self = this;
options = this.filterOptions(options, 'edit');
edit: function (data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'edit'),
self = this;
if (!Array.isArray(data)) {
data = [data];
@ -146,10 +146,13 @@ Settings = ghostBookshelf.Model.extend({
});
},
populateDefaults: function populateDefaults(options) {
var self = this;
populateDefaults: function populateDefaults(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'populateDefaults'),
self = this;
options = _.merge({}, options || {}, internalContext);
if (!options.context) {
options.context = internalContext.context;
}
return this
.findAll(options)

View File

@ -75,11 +75,11 @@ Subscriber = ghostBookshelf.Model.extend({
},
// TODO: This is a copy paste of models/user.js!
getByEmail: function getByEmail(email, options) {
options = options || {};
getByEmail: function getByEmail(email, unfilteredOptions) {
var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'getByEmail');
options.require = true;
return Subscribers.forge(options).fetch(options).then(function then(subscribers) {
return Subscribers.forge().fetch(options).then(function then(subscribers) {
var subscriberWithEmail = subscribers.find(function findSubscriber(subscriber) {
return subscriber.get('email').toLowerCase() === email.toLowerCase();
});

View File

@ -1,5 +1,4 @@
var _ = require('lodash'),
ghostBookshelf = require('./base'),
var ghostBookshelf = require('./base'),
common = require('../lib/common'),
Tag,
Tags;
@ -54,10 +53,9 @@ Tag = ghostBookshelf.Model.extend({
return this.belongsToMany('Post');
},
toJSON: function toJSON(options) {
options = options || {};
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
toJSON: function toJSON(unfilteredOptions) {
var options = Tag.filterOptions(unfilteredOptions, 'toJSON'),
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
attrs.parent = attrs.parent || attrs.parent_id;
delete attrs.parent_id;
@ -94,29 +92,11 @@ Tag = ghostBookshelf.Model.extend({
return options;
},
/**
* ### Find One
* @overrides ghostBookshelf.Model.findOne
*/
findOne: function findOne(data, options) {
options = options || {};
destroy: function destroy(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
options.withRelated = ['posts'];
options = this.filterOptions(options, 'findOne');
data = this.filterData(data, 'findOne');
var tag = this.forge(data);
// Add related objects
options.withRelated = _.union(options.withRelated, options.include);
return tag.fetch(options);
},
destroy: function destroy(options) {
var id = options.id;
options = this.filterOptions(options, 'destroy');
return this.forge({id: id}).fetch({withRelated: ['posts']}).then(function destroyTagsAndPost(tag) {
return this.forge({id: options.id}).fetch(options).then(function destroyTagsAndPost(tag) {
return tag.related('posts').detach().then(function destroyTags() {
return tag.destroy(options);
});

View File

@ -201,10 +201,9 @@ User = ghostBookshelf.Model.extend({
return validation.validateSchema(this.tableName, userData);
},
toJSON: function toJSON(options) {
options = options || {};
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
toJSON: function toJSON(unfilteredOptions) {
var options = User.filterOptions(unfilteredOptions, 'toJSON'),
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
// remove password hash for security reasons
delete attrs.password;
@ -341,11 +340,11 @@ User = ghostBookshelf.Model.extend({
permittedOptionsToReturn = permittedOptionsToReturn.concat(validOptions[methodName]);
}
// CASE: The `include` parameter is allowed when using the public API, but not the `roles` value.
// CASE: The `withRelated` parameter is allowed when using the public API, but not the `roles` value.
// Otherwise we expose too much information.
if (options && options.context && options.context.public) {
if (options.include && options.include.indexOf('roles') !== -1) {
options.include.splice(options.include.indexOf('roles'), 1);
if (options.withRelated && options.withRelated.indexOf('roles') !== -1) {
options.withRelated.splice(options.withRelated.indexOf('roles'), 1);
}
}
@ -358,11 +357,14 @@ User = ghostBookshelf.Model.extend({
* We have to clone the data, because we remove values from this object.
* This is not expected from outside!
*
* @TODO: use base class
*
* @extends ghostBookshelf.Model.findOne to include roles
* **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
*/
findOne: function findOne(dataToClone, options) {
var query,
findOne: function findOne(dataToClone, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'findOne'),
query,
status,
data = _.cloneDeep(dataToClone),
lookupRole = data.role;
@ -376,15 +378,12 @@ User = ghostBookshelf.Model.extend({
delete data.status;
data = this.filterData(data);
options = this.filterOptions(options, 'findOne');
options.withRelated = _.union(options.withRelated, options.include);
// Support finding by role
if (lookupRole) {
options.withRelated = _.union(options.withRelated, ['roles']);
options.include = _.union(options.include, ['roles']);
query = this.forge(data);
query.query('join', 'roles_users', 'users.id', '=', 'roles_users.user_id');
query.query('join', 'roles', 'roles_users.role_id', '=', 'roles.id');
query.query('where', 'roles.name', '=', lookupRole);
@ -409,8 +408,9 @@ User = ghostBookshelf.Model.extend({
* @extends ghostBookshelf.Model.edit to handle returning the full object
* **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
*/
edit: function edit(data, options) {
var self = this,
edit: function edit(data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'edit'),
self = this,
ops = [];
if (data.roles && data.roles.length > 1) {
@ -419,9 +419,6 @@ User = ghostBookshelf.Model.extend({
);
}
options = options || {};
options.withRelated = _.union(options.withRelated, options.include);
if (data.email) {
ops.push(function checkForDuplicateEmail() {
return self.getByEmail(data.email, options).then(function then(user) {
@ -476,19 +473,17 @@ User = ghostBookshelf.Model.extend({
* This is not expected from outside!
*
* @param {object} dataToClone
* @param {object} options
* @param {object} unfilteredOptions
* @extends ghostBookshelf.Model.add to manage all aspects of user signup
* **See:** [ghostBookshelf.Model.add](base.js.html#Add)
*/
add: function add(dataToClone, options) {
var self = this,
add: function add(dataToClone, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'add'),
self = this,
data = _.cloneDeep(dataToClone),
userData = this.filterData(data),
roles;
options = this.filterOptions(options, 'add');
options.withRelated = _.union(options.withRelated, options.include);
// check for too many roles
if (data.roles && data.roles.length > 1) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.models.user.onlyOneRolePerUserSupported')}));
@ -556,8 +551,9 @@ User = ghostBookshelf.Model.extend({
* Owner already has a slug -> force setting a new one by setting slug to null
* @TODO: kill setup function?
*/
setup: function setup(data, options) {
var self = this,
setup: function setup(data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'setup'),
self = this,
userData = this.filterData(data),
passwordValidation = {};
@ -567,9 +563,6 @@ User = ghostBookshelf.Model.extend({
return Promise.reject(new common.errors.ValidationError({message: passwordValidation.message}));
}
options = this.filterOptions(options, 'setup');
options.withRelated = _.union(options.withRelated, options.include);
userData.slug = null;
return self.edit(userData, options);
},
@ -624,7 +617,7 @@ User = ghostBookshelf.Model.extend({
return this.findOne({
id: userModelOrId,
status: 'all'
}, {include: ['roles']}).then(function then(foundUserModel) {
}, {withRelated: ['roles']}).then(function then(foundUserModel) {
if (!foundUserModel) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.models.user.userNotFound')
@ -753,17 +746,20 @@ User = ghostBookshelf.Model.extend({
/**
* Naive change password method
* @param {Object} object
* @param {Object} options
* @param {Object} unfilteredOptions
*/
changePassword: function changePassword(object, options) {
var self = this,
changePassword: function changePassword(object, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'changePassword'),
self = this,
newPassword = object.newPassword,
userId = object.user_id,
oldPassword = object.oldPassword,
isLoggedInUser = userId === options.context.user,
user;
return self.forge({id: userId}).fetch({require: true})
options.require = true;
return self.forge({id: userId}).fetch(options)
.then(function then(_user) {
user = _user;
@ -779,13 +775,14 @@ User = ghostBookshelf.Model.extend({
});
},
transferOwnership: function transferOwnership(object, options) {
var ownerRole,
transferOwnership: function transferOwnership(object, unfilteredOptions) {
var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'transferOwnership'),
ownerRole,
contextUser;
return Promise.join(
ghostBookshelf.model('Role').findOne({name: 'Owner'}),
User.findOne({id: options.context.user}, {include: ['roles']})
User.findOne({id: options.context.user}, {withRelated: ['roles']})
)
.then(function then(results) {
ownerRole = results[0];
@ -798,7 +795,7 @@ User = ghostBookshelf.Model.extend({
}
return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
User.findOne({id: object.id}, {include: ['roles']}));
User.findOne({id: object.id}, {withRelated: ['roles']}));
})
.then(function then(results) {
var adminRole = results[0],
@ -820,7 +817,7 @@ User = ghostBookshelf.Model.extend({
.fetch({withRelated: ['roles']});
})
.then(function then(users) {
options.include = ['roles'];
options.withRelated = ['roles'];
return users.toJSON(options);
});
},
@ -828,15 +825,16 @@ User = ghostBookshelf.Model.extend({
// Get the user by email address, enforces case insensitivity rejects if the user is not found
// When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
// joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
getByEmail: function getByEmail(email, options) {
options = options || {};
getByEmail: function getByEmail(email, unfilteredOptions) {
var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'getByEmail');
// We fetch all users and process them in JS as there is no easy way to make this query across all DBs
// Although they all support `lower()`, sqlite can't case transform unicode characters
// This is somewhat mute, as validator.isEmail() also doesn't support unicode, but this is much easier / more
// likely to be fixed in the near future.
options.require = true;
return Users.forge(options).fetch(options).then(function then(users) {
return Users.forge().fetch(options).then(function then(users) {
var userWithEmail = users.find(function findUser(user) {
return user.get('email').toLowerCase() === email.toLowerCase();
});

View File

@ -25,21 +25,20 @@ Webhook = ghostBookshelf.Model.extend({
model.emitChange('deleted', options);
}
}, {
findAllByEvent: function findAllByEvent(event, options) {
var webhooksCollection = Webhooks.forge();
options = this.filterOptions(options, 'findAll');
findAllByEvent: function findAllByEvent(event, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'findAll'),
webhooksCollection = Webhooks.forge();
return webhooksCollection
.query('where', 'event', '=', event)
.fetch(options);
},
getByEventAndTarget: function getByEventAndTarget(event, targetUrl, options) {
options = options || {};
getByEventAndTarget: function getByEventAndTarget(event, targetUrl, unfilteredOptions) {
var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'getByEventAndTarget');
options.require = true;
return Webhooks.forge(options).fetch(options).then(function then(webhooks) {
return Webhooks.forge().fetch(options).then(function then(webhooks) {
var webhookWithEventAndTarget = webhooks.find(function findWebhook(webhook) {
return webhook.get('event').toLowerCase() === event.toLowerCase()
&& webhook.get('target_url').toLowerCase() === targetUrl.toLowerCase();

View File

@ -19,7 +19,7 @@ strategies = {
return models.Client.findOne({slug: clientId}, {withRelated: ['trustedDomains']})
.then(function then(model) {
if (model) {
var client = model.toJSON({include: ['trustedDomains']});
var client = model.toJSON({withRelated: ['trustedDomains']});
if (client.status === 'enabled' && client.secret === clientSecret) {
return done(null, client);
}

View File

@ -5,7 +5,7 @@ var _ = require('lodash'),
module.exports = {
user: function (id) {
return models.User.findOne({id: id, status: 'all'}, {include: ['permissions', 'roles', 'roles.permissions']})
return models.User.findOne({id: id, status: 'all'}, {withRelated: ['permissions', 'roles', 'roles.permissions']})
.then(function (foundUser) {
// CASE: {context: {user: id}} where the id is not in our database
if (!foundUser) {

View File

@ -286,7 +286,7 @@ describe('Authentication API', function () {
should.not.exist(_invite);
return models.User.findOne({
email: invite.get('email')
}, _.merge({include: ['roles']}, context.internal));
}, _.merge({withRelated: ['roles']}, context.internal));
})
.then(function (user) {
user.toJSON().roles.length.should.eql(1);

View File

@ -317,7 +317,7 @@ describe('Invites API', function () {
role_id: testUtils.roles.ids.admin
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.admin)).then(function (response) {
}, _.merge({}, {include: 'roles'}, testUtils.context.admin)).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.admin);
done();

View File

@ -864,7 +864,7 @@ describe('Users API', function () {
return models.Post.findAll(_.merge({}, {
context: context.editor.context,
filter: 'author_id:' + userIdFor.editor,
include: ['tags']
withRelated: ['tags']
}, options));
}).then(function (posts) {
posts.models.length.should.eql(3);
@ -877,7 +877,7 @@ describe('Users API', function () {
return models.Post.findAll(_.merge({
context: context.author.context,
filter: 'author_id:' + userIdFor.author,
include: ['tags']
withRelated: ['tags']
}, options));
}).then(function (posts) {
posts.models.length.should.eql(3);

View File

@ -534,7 +534,7 @@ describe('Import', function () {
// Grab the data from tables
// NOTE: we have to return sorted data, sqlite can insert the posts in a different order
return Promise.all([
models.Post.findPage({include: ['tags']}),
models.Post.findPage({withRelated: ['tags']}),
models.Tag.findAll()
]);
}).then(function (importedData) {

View File

@ -170,16 +170,16 @@ describe('Database Migration (special functions)', function () {
it('should populate all fixtures correctly', function () {
var props = {
posts: Models.Post.findAll({include: ['tags']}),
posts: Models.Post.findAll({withRelated: ['tags']}),
tags: Models.Tag.findAll(),
users: Models.User.findAll({
filter: 'status:inactive',
context: {internal: true},
include: ['roles']
withRelated: ['roles']
}),
clients: Models.Client.findAll(),
roles: Models.Role.findAll(),
permissions: Models.Permission.findAll({include: ['roles']})
permissions: Models.Permission.findAll({withRelated: ['roles']})
};
return Promise.props(props).then(function (result) {

View File

@ -127,7 +127,7 @@ describe('Permission Model', function () {
// return testUser.permissions().attach(testPermission);
// });
// }).then(function () {
// return Models.User.findOne({id: 1}, { include: ['permissions']});
// return Models.User.findOne({id: 1}, { withRelated: ['permissions']});
// }).then(function (updatedUser) {
// should.exist(updatedUser);

View File

@ -105,7 +105,7 @@ describe('Post Model', function () {
});
it('can findAll, returning all related data', function (done) {
PostModel.findAll({include: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
PostModel.findAll({withRelated: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
should.exist(results);
results.length.should.be.above(0);
@ -123,7 +123,7 @@ describe('Post Model', function () {
it('can findAll, use formats option', function (done) {
var options = {
formats: ['mobiledoc', 'plaintext'],
include: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']
withRelated: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']
};
PostModel.findAll(options)
@ -165,7 +165,7 @@ describe('Post Model', function () {
});
it('can findPage, returning all related data', function (done) {
PostModel.findPage({include: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
PostModel.findPage({withRelated: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
should.exist(results);
@ -361,7 +361,7 @@ describe('Post Model', function () {
it('can findOne, returning all related data', function (done) {
var firstPost;
PostModel.findOne({}, {include: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
PostModel.findOne({}, {withRelated: ['author', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (result) {
should.exist(result);
firstPost = result.toJSON();
@ -1417,7 +1417,7 @@ describe('Post Model', function () {
var firstItemData = {id: testUtils.DataGenerator.Content.posts[0].id};
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData, {include: ['tags']}).then(function (results) {
PostModel.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
@ -1456,7 +1456,7 @@ describe('Post Model', function () {
var firstItemData = {id: testUtils.DataGenerator.Content.posts[3].id, status: 'draft'};
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData, {include: ['tags']}).then(function (results) {
PostModel.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
@ -1493,7 +1493,7 @@ describe('Post Model', function () {
var firstItemData = {id: testUtils.DataGenerator.Content.posts[5].id};
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData, {include: ['tags']}).then(function (results) {
PostModel.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
var page;
should.exist(results);
page = results.toJSON();
@ -1531,7 +1531,7 @@ describe('Post Model', function () {
var firstItemData = {id: testUtils.DataGenerator.Content.posts[6].id, status: 'draft'};
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData, {include: ['tags']}).then(function (results) {
PostModel.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
var page;
should.exist(results);
page = results.toJSON();
@ -1724,7 +1724,7 @@ describe('Post Model', function () {
tag2: TagModel.add(extraTags[1], context),
tag3: TagModel.add(extraTags[2], context)
}).then(function (result) {
postJSON = result.post.toJSON({include: ['tags']});
postJSON = result.post.toJSON({withRelated: ['tags']});
tagJSON.push(result.tag1.toJSON());
tagJSON.push(result.tag2.toJSON());
tagJSON.push(result.tag3.toJSON());
@ -1765,7 +1765,7 @@ describe('Post Model', function () {
// Edit the post
return PostModel.edit(newJSON, editOptions).then(function (updatedPost) {
updatedPost = updatedPost.toJSON({include: ['tags']});
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
updatedPost.tags.should.have.lengthOf(1);
updatedPost.tags[0].name.should.eql(postJSON.tags[0].name);
@ -1793,7 +1793,7 @@ describe('Post Model', function () {
return PostModel.edit(newJSON, editOptions);
})
.then(function (updatedPost) {
updatedPost = updatedPost.toJSON({include: ['tags']});
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
updatedPost.tags.should.have.lengthOf(1);
updatedPost.tags[0].should.have.properties({
@ -1823,7 +1823,7 @@ describe('Post Model', function () {
// Edit the post
return PostModel.edit(newJSON, editOptions).then(function (updatedPost) {
updatedPost = updatedPost.toJSON({include: ['tags']});
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
updatedPost.tags.should.have.lengthOf(3);
updatedPost.tags[0].should.have.properties({
@ -1853,7 +1853,7 @@ describe('Post Model', function () {
// Edit the post
return PostModel.edit(newJSON, editOptions).then(function (updatedPost) {
updatedPost = updatedPost.toJSON({include: ['tags']});
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
updatedPost.tags.should.have.lengthOf(3);
@ -1873,7 +1873,7 @@ describe('Post Model', function () {
// Edit the post
return PostModel.edit(newJSON, editOptions).then(function (updatedPost) {
updatedPost = updatedPost.toJSON({include: ['tags']});
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
updatedPost.tags.should.have.lengthOf(1);
});

View File

@ -47,7 +47,7 @@ describe('Tag Model', function () {
it('returns count.posts if include count.posts', function (done) {
testUtils.fixtures.insertPostsAndTags().then(function () {
TagModel.findOne({slug: 'kitchen-sink'}, {include: 'count.posts'}).then(function (tag) {
TagModel.findOne({slug: 'kitchen-sink'}, {withRelated: ['count.posts']}).then(function (tag) {
should.exist(tag);
tag.toJSON().count.posts.should.equal(2);
@ -75,7 +75,7 @@ describe('Tag Model', function () {
});
it('with include count.posts', function (done) {
TagModel.findPage({limit: 'all', include: 'count.posts'}).then(function (results) {
TagModel.findPage({limit: 'all', withRelated: ['count.posts']}).then(function (results) {
results.meta.pagination.page.should.equal(1);
results.meta.pagination.limit.should.equal('all');
results.meta.pagination.pages.should.equal(1);

View File

@ -348,7 +348,7 @@ describe('User Model', function run() {
RoleModel.findOne().then(function (role) {
userData.roles = [role.toJSON()];
return UserModel.add(userData, _.extend({}, context, {include: ['roles']}));
return UserModel.add(userData, _.extend({}, context, {withRelated: ['roles']}));
}).then(function (createdUser) {
should.exist(createdUser);
createdUser.get('password').should.not.equal(userData.password, 'password was hashed');
@ -371,7 +371,7 @@ describe('User Model', function run() {
RoleModel.findOne().then(function (role) {
userData.roles = [role.toJSON()];
return UserModel.add(userData, _.extend({}, context, {include: ['roles']}));
return UserModel.add(userData, _.extend({}, context, {withRelated: ['roles']}));
}).then(function () {
done(new Error('User was created with an invalid email address'));
}).catch(function () {

View File

@ -298,14 +298,15 @@ describe('API Utils', function () {
allowed = ['a', 'b', 'c'],
options = {include: 'a,b'},
actualResult;
actualResult = apiUtils.convertOptions(allowed)(_.clone(options));
prepareIncludeStub.calledOnce.should.be.true();
prepareIncludeStub.calledWith(options.include, allowed).should.be.true();
actualResult.should.have.hasOwnProperty('include');
actualResult.include.should.be.an.Array();
actualResult.include.should.eql(expectedResult);
actualResult.should.have.hasOwnProperty('withRelated');
actualResult.withRelated.should.be.an.Array();
actualResult.withRelated.should.eql(expectedResult);
});
});

View File

@ -48,8 +48,9 @@ describe('Permission Providers', function () {
roles: fakeAdminRole,
permissions: fakeAdminRolePermissions
};
// We use this inside toJSON.
fakeUser.include = ['roles', 'permissions', 'roles.permissions'];
fakeUser.withRelated = ['roles', 'permissions', 'roles.permissions'];
return Promise.resolve(fakeUser);
});
@ -96,7 +97,7 @@ describe('Permission Providers', function () {
roles: fakeAdminRole
};
// We use this inside toJSON.
fakeUser.include = ['roles', 'permissions', 'roles.permissions'];
fakeUser.withRelated = ['roles', 'permissions', 'roles.permissions'];
return Promise.resolve(fakeUser);
});
@ -144,7 +145,7 @@ describe('Permission Providers', function () {
permissions: fakeAdminRolePermissions
};
// We use this inside toJSON.
fakeUser.include = ['roles', 'permissions', 'roles.permissions'];
fakeUser.withRelated = ['roles', 'permissions', 'roles.permissions'];
return Promise.resolve(fakeUser);
});