2014-06-03 17:05:25 +04:00
|
|
|
// # API Utils
|
|
|
|
// Shared helpers for working with the API
|
2018-09-18 16:59:56 +03:00
|
|
|
const Promise = require('bluebird'),
|
2017-09-12 18:31:14 +03:00
|
|
|
_ = require('lodash'),
|
2018-09-27 17:06:57 +03:00
|
|
|
permissions = require('../../services/permissions'),
|
|
|
|
validation = require('../../data/validation'),
|
2018-11-19 20:08:48 +03:00
|
|
|
sharedUtils = require('../shared/utils'),
|
2018-09-27 17:06:57 +03:00
|
|
|
common = require('../../lib/common');
|
2018-09-18 16:59:56 +03:00
|
|
|
|
|
|
|
let utils;
|
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
2014-05-08 16:41:19 +04:00
|
|
|
|
|
|
|
utils = {
|
2015-07-01 21:17:56 +03:00
|
|
|
// ## Default Options
|
|
|
|
// Various default options for different types of endpoints
|
|
|
|
|
|
|
|
// ### Auto Default Options
|
|
|
|
// Handled / Added automatically by the validate function
|
|
|
|
// globalDefaultOptions - valid for every api endpoint
|
|
|
|
globalDefaultOptions: ['context', 'include'],
|
|
|
|
// dataDefaultOptions - valid for all endpoints which take object as well as options
|
|
|
|
dataDefaultOptions: ['data'],
|
|
|
|
|
|
|
|
// ### Manual Default Options
|
|
|
|
// These must be provided by the endpoint
|
|
|
|
// browseDefaultOptions - valid for all browse api endpoints
|
2015-11-12 17:21:04 +03:00
|
|
|
browseDefaultOptions: ['page', 'limit', 'fields', 'filter', 'order', 'debug'],
|
2015-07-01 21:17:56 +03:00
|
|
|
// idDefaultOptions - valid whenever an id is valid
|
|
|
|
idDefaultOptions: ['id'],
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ## Validate
|
|
|
|
* Prepare to validate the object and options passed to an endpoint
|
|
|
|
* @param {String} docName
|
|
|
|
* @param {Object} extras
|
|
|
|
* @returns {Function} doValidate
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
validate(docName, extras) {
|
2015-07-01 21:17:56 +03:00
|
|
|
/**
|
|
|
|
* ### Do Validate
|
|
|
|
* Validate the object and options passed to an endpoint
|
2015-10-12 19:54:15 +03:00
|
|
|
* @argument {...*} [arguments] object or object and options hash
|
2015-07-01 21:17:56 +03:00
|
|
|
*/
|
2015-06-22 23:11:35 +03:00
|
|
|
return function doValidate() {
|
2018-09-18 16:59:56 +03:00
|
|
|
let object, options, permittedOptions;
|
2016-05-19 14:49:22 +03:00
|
|
|
|
2015-06-22 23:11:35 +03:00
|
|
|
if (arguments.length === 2) {
|
|
|
|
object = arguments[0];
|
|
|
|
options = _.clone(arguments[1]) || {};
|
|
|
|
} else if (arguments.length === 1) {
|
|
|
|
options = _.clone(arguments[0]) || {};
|
|
|
|
} else {
|
|
|
|
options = {};
|
|
|
|
}
|
|
|
|
|
2015-07-01 21:17:56 +03:00
|
|
|
// Setup permitted options, starting with the global defaults
|
|
|
|
permittedOptions = utils.globalDefaultOptions;
|
|
|
|
|
|
|
|
// Add extra permitted options if any are passed in
|
|
|
|
if (extras && extras.opts) {
|
|
|
|
permittedOptions = permittedOptions.concat(extras.opts);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This request will have a data key added during validation
|
|
|
|
if ((extras && extras.attrs) || object) {
|
|
|
|
permittedOptions = permittedOptions.concat(utils.dataDefaultOptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If an 'attrs' object is passed, we use this to pick from options and convert them to data
|
|
|
|
if (extras && extras.attrs) {
|
|
|
|
options.data = _.pick(options, extras.attrs);
|
|
|
|
options = _.omit(options, extras.attrs);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Check Options
|
|
|
|
* Ensure that the options provided match exactly with what is permitted
|
|
|
|
* - incorrect option keys are sanitized
|
|
|
|
* - incorrect option values are validated
|
|
|
|
* @param {object} options
|
|
|
|
* @returns {Promise<options>}
|
|
|
|
*/
|
|
|
|
function checkOptions(options) {
|
|
|
|
// @TODO: should we throw an error if there are incorrect options provided?
|
|
|
|
options = _.pick(options, permittedOptions);
|
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
let validationErrors = utils.validateOptions(options);
|
2015-07-01 21:17:56 +03:00
|
|
|
|
|
|
|
if (_.isEmpty(validationErrors)) {
|
|
|
|
return Promise.resolve(options);
|
|
|
|
}
|
|
|
|
|
2015-09-22 19:38:30 +03:00
|
|
|
// For now, we can only handle showing the first validation error
|
2016-10-04 18:33:43 +03:00
|
|
|
return Promise.reject(validationErrors[0]);
|
2015-06-22 23:11:35 +03:00
|
|
|
}
|
|
|
|
|
2015-07-01 21:17:56 +03:00
|
|
|
// If we got an object, check that too
|
2015-06-22 23:11:35 +03:00
|
|
|
if (object) {
|
2018-09-18 16:59:56 +03:00
|
|
|
return utils.checkObject(object, docName, options.id).then((data) => {
|
2015-06-22 23:11:35 +03:00
|
|
|
options.data = data;
|
2015-07-01 21:17:56 +03:00
|
|
|
|
|
|
|
return checkOptions(options);
|
2015-06-22 23:11:35 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-07-01 21:17:56 +03:00
|
|
|
// Otherwise just check options and return
|
|
|
|
return checkOptions(options);
|
2015-06-22 23:11:35 +03:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
validateOptions(options) {
|
|
|
|
let globalValidations = {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
id: {matches: /^[a-f\d]{24}$|^1$|me/i},
|
2015-07-01 21:17:56 +03:00
|
|
|
uuid: {isUUID: true},
|
2015-09-22 19:38:30 +03:00
|
|
|
slug: {isSlug: true},
|
2015-07-01 21:17:56 +03:00
|
|
|
page: {matches: /^\d+$/},
|
|
|
|
limit: {matches: /^\d+|all$/},
|
2016-05-19 14:49:22 +03:00
|
|
|
from: {isDate: true},
|
|
|
|
to: {isDate: true},
|
2015-10-22 17:08:15 +03:00
|
|
|
fields: {matches: /^[\w, ]+$/},
|
2018-09-17 21:49:30 +03:00
|
|
|
order: {matches: /^[a-z0-9_,. ]+$/i},
|
2017-11-14 14:09:41 +03:00
|
|
|
name: {},
|
|
|
|
email: {isEmail: true}
|
2015-07-01 21:17:56 +03:00
|
|
|
},
|
|
|
|
// these values are sanitised/validated separately
|
2017-05-30 13:40:39 +03:00
|
|
|
noValidation = ['data', 'context', 'include', 'filter', 'forUpdate', 'transacting', 'formats'],
|
2015-07-01 21:17:56 +03:00
|
|
|
errors = [];
|
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
_.each(options, (value, key) => {
|
2015-07-01 21:17:56 +03:00
|
|
|
// data is validated elsewhere
|
|
|
|
if (noValidation.indexOf(key) === -1) {
|
|
|
|
if (globalValidations[key]) {
|
|
|
|
errors = errors.concat(validation.validate(value, key, globalValidations[key]));
|
|
|
|
} else {
|
2015-09-22 19:38:30 +03:00
|
|
|
// all other keys should be alpha-numeric with dashes/underscores, like tag, author, status, etc
|
|
|
|
errors = errors.concat(validation.validate(value, key, globalValidations.slug));
|
2015-07-01 21:17:56 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
},
|
|
|
|
|
2015-06-27 21:09:25 +03:00
|
|
|
/**
|
2015-11-11 21:12:18 +03:00
|
|
|
* ## Detect Public Context
|
|
|
|
* Calls parse context to expand the options.context object
|
2015-06-27 21:09:25 +03:00
|
|
|
* @param {Object} options
|
|
|
|
* @returns {Boolean}
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
detectPublicContext(options) {
|
2015-11-11 21:12:18 +03:00
|
|
|
options.context = permissions.parseContext(options.context);
|
|
|
|
return options.context.public;
|
2015-06-27 21:09:25 +03:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* ## Apply Public Permissions
|
|
|
|
* Update the options object so that the rules reflect what is permitted to be retrieved from a public request
|
|
|
|
* @param {String} docName
|
|
|
|
* @param {String} method (read || browse)
|
|
|
|
* @param {Object} options
|
|
|
|
* @returns {Object} options
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
applyPublicPermissions(docName, method, options) {
|
2015-06-27 21:09:25 +03:00
|
|
|
return permissions.applyPublicRules(docName, method, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ## Handle Public Permissions
|
|
|
|
* @param {String} docName
|
|
|
|
* @param {String} method (read || browse)
|
|
|
|
* @returns {Function}
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
handlePublicPermissions(docName, method) {
|
|
|
|
let singular = docName.replace(/s$/, '');
|
2015-06-27 21:09:25 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if this is a public request, if so use the public permissions, otherwise use standard canThis
|
|
|
|
* @param {Object} options
|
|
|
|
* @returns {Object} options
|
|
|
|
*/
|
|
|
|
return function doHandlePublicPermissions(options) {
|
2018-09-18 16:59:56 +03:00
|
|
|
let permsPromise;
|
2015-06-27 21:09:25 +03:00
|
|
|
|
2015-11-11 21:12:18 +03:00
|
|
|
if (utils.detectPublicContext(options)) {
|
2015-06-27 21:09:25 +03:00
|
|
|
permsPromise = utils.applyPublicPermissions(docName, method, options);
|
|
|
|
} else {
|
|
|
|
permsPromise = permissions.canThis(options.context)[method][singular](options.data);
|
|
|
|
}
|
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
return permsPromise.then(() => {
|
2015-06-27 21:09:25 +03:00
|
|
|
return options;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2015-08-11 17:03:57 +03:00
|
|
|
/**
|
|
|
|
* ## Handle Permissions
|
|
|
|
* @param {String} docName
|
|
|
|
* @param {String} method (browse || read || edit || add || destroy)
|
2018-07-10 13:20:43 +03:00
|
|
|
* @param {Array} unsafeAttrNames - attribute names (e.g. post.status) that could change the outcome
|
2015-08-11 17:03:57 +03:00
|
|
|
* @returns {Function}
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
handlePermissions(docName, method, unsafeAttrNames) {
|
|
|
|
let singular = docName.replace(/s$/, '');
|
2015-08-11 17:03:57 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Handle Permissions
|
|
|
|
* We need to be an authorised user to perform this action
|
|
|
|
* @param {Object} options
|
|
|
|
* @returns {Object} options
|
|
|
|
*/
|
|
|
|
return function doHandlePermissions(options) {
|
2018-09-18 16:59:56 +03:00
|
|
|
let unsafeAttrObject = unsafeAttrNames && _.has(options, `data.[${docName}][0]`) ? _.pick(options.data[docName][0], unsafeAttrNames) : {},
|
2017-09-26 19:06:14 +03:00
|
|
|
permsPromise = permissions.canThis(options.context)[method][singular](options.id, unsafeAttrObject);
|
2015-08-11 17:03:57 +03:00
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
return permsPromise.then((result) => {
|
2018-02-07 12:46:22 +03:00
|
|
|
/*
|
|
|
|
* Allow the permissions function to return a list of excluded attributes.
|
|
|
|
* If it does, omit those attrs from the data passed through
|
|
|
|
*
|
|
|
|
* NOTE: excludedAttrs differ from unsafeAttrs in that they're determined by the model's permissible function,
|
|
|
|
* and the attributes are simply excluded rather than throwing a NoPermission exception
|
|
|
|
*
|
|
|
|
* TODO: This is currently only needed because of the posts model and the contributor role. Once we extend the
|
|
|
|
* contributor role to be able to edit existing tags, this concept can be removed.
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
if (result && result.excludedAttrs && _.has(options, `data.[${docName}][0]`)) {
|
2018-02-07 12:46:22 +03:00
|
|
|
options.data[docName][0] = _.omit(options.data[docName][0], result.excludedAttrs);
|
|
|
|
}
|
|
|
|
|
2015-08-11 17:03:57 +03:00
|
|
|
return options;
|
2018-09-18 16:59:56 +03:00
|
|
|
}).catch((err) => {
|
2017-12-12 00:47:46 +03:00
|
|
|
if (err instanceof common.errors.NoPermissionError) {
|
|
|
|
err.message = common.i18n.t('errors.api.utils.noPermissionToCall', {
|
|
|
|
method: method,
|
|
|
|
docName: docName
|
|
|
|
});
|
2016-10-06 15:27:35 +03:00
|
|
|
return Promise.reject(err);
|
|
|
|
}
|
|
|
|
|
2018-02-21 18:59:48 +03:00
|
|
|
if (common.errors.utils.isIgnitionError(err)) {
|
|
|
|
return Promise.reject(err);
|
|
|
|
}
|
|
|
|
|
2017-12-12 00:47:46 +03:00
|
|
|
return Promise.reject(new common.errors.GhostError({
|
2016-10-06 15:27:35 +03:00
|
|
|
err: err
|
|
|
|
}));
|
2015-08-11 17:03:57 +03:00
|
|
|
});
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2018-11-19 20:08:48 +03:00
|
|
|
trimAndLowerCase: sharedUtils.options.trimAndLowerCase,
|
2015-06-27 21:09:25 +03:00
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
prepareInclude(include, allowedIncludes) {
|
2015-10-22 17:08:15 +03:00
|
|
|
return _.intersection(this.trimAndLowerCase(include), allowedIncludes);
|
|
|
|
},
|
2015-07-04 21:27:23 +03:00
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
prepareFields(fields) {
|
2015-10-22 17:08:15 +03:00
|
|
|
return this.trimAndLowerCase(fields);
|
2015-07-04 21:27:23 +03:00
|
|
|
},
|
|
|
|
|
2018-09-18 16:59:56 +03:00
|
|
|
prepareFormats(formats, allowedFormats) {
|
2017-05-30 13:40:39 +03:00
|
|
|
return _.intersection(this.trimAndLowerCase(formats), allowedFormats);
|
|
|
|
},
|
|
|
|
|
2015-06-22 23:11:35 +03:00
|
|
|
/**
|
2015-06-27 21:09:25 +03:00
|
|
|
* ## Convert Options
|
2015-06-22 23:11:35 +03:00
|
|
|
* @param {Array} allowedIncludes
|
|
|
|
* @returns {Function} doConversion
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
convertOptions(allowedIncludes, allowedFormats, convertOptions = {forModel: true}) {
|
2015-06-22 23:11:35 +03:00
|
|
|
/**
|
2018-07-30 18:21:52 +03:00
|
|
|
* Convert our options from API-style to Model-style (default)
|
2015-06-22 23:11:35 +03:00
|
|
|
* @param {Object} options
|
|
|
|
* @returns {Object} options
|
|
|
|
*/
|
|
|
|
return function doConversion(options) {
|
|
|
|
if (options.include) {
|
2018-07-30 18:21:52 +03:00
|
|
|
if (!convertOptions.forModel) {
|
|
|
|
options.include = utils.prepareInclude(options.include, allowedIncludes);
|
|
|
|
} else {
|
|
|
|
options.withRelated = utils.prepareInclude(options.include, allowedIncludes);
|
|
|
|
delete options.include;
|
|
|
|
}
|
2015-06-22 23:11:35 +03:00
|
|
|
}
|
2017-05-30 13:40:39 +03:00
|
|
|
|
2015-07-04 21:27:23 +03:00
|
|
|
if (options.fields) {
|
|
|
|
options.columns = utils.prepareFields(options.fields);
|
|
|
|
delete options.fields;
|
|
|
|
}
|
2017-04-19 16:53:23 +03:00
|
|
|
|
2017-05-30 13:40:39 +03:00
|
|
|
if (options.formats) {
|
|
|
|
options.formats = utils.prepareFormats(options.formats, allowedFormats);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.formats && options.columns) {
|
|
|
|
options.columns = options.columns.concat(options.formats);
|
|
|
|
}
|
|
|
|
|
2015-06-22 23:11:35 +03:00
|
|
|
return options;
|
|
|
|
};
|
|
|
|
},
|
2014-06-03 17:05:25 +04:00
|
|
|
/**
|
|
|
|
* ### Check Object
|
|
|
|
* Check an object passed to the API is in the correct format
|
|
|
|
*
|
2018-04-05 16:59:52 +03:00
|
|
|
* @TODO:
|
|
|
|
* The weird thing about this function is..
|
|
|
|
* - that the API converts properties back to model notation
|
|
|
|
* - post.author -> post.author_id
|
|
|
|
* - and the model layer implementation of `toJSON` knows about these transformations as well
|
|
|
|
* - post.author_id -> post.author
|
|
|
|
* - this must live in one place
|
|
|
|
* - API IN <-> API OUT
|
|
|
|
* - this should be unrelated to the model layer
|
|
|
|
*
|
2014-06-03 17:05:25 +04:00
|
|
|
* @param {Object} object
|
|
|
|
* @param {String} docName
|
|
|
|
* @returns {Promise(Object)} resolves to the original object if it checks out
|
|
|
|
*/
|
2018-09-18 16:59:56 +03:00
|
|
|
checkObject(object, docName, editId) {
|
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
2014-05-08 16:41:19 +04:00
|
|
|
if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) {
|
2017-12-12 00:47:46 +03:00
|
|
|
return Promise.reject(new common.errors.BadRequestError({
|
|
|
|
message: common.i18n.t('errors.api.utils.noRootKeyProvided', {docName: docName})
|
2016-10-06 15:27:35 +03: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
2014-05-08 16:41:19 +04:00
|
|
|
}
|
2014-07-18 12:48:48 +04:00
|
|
|
|
|
|
|
if (docName === 'posts') {
|
2018-03-27 17:16:15 +03:00
|
|
|
/**
|
|
|
|
* Convert author property to author_id to match the name in the database.
|
|
|
|
*
|
2018-08-03 16:14:38 +03:00
|
|
|
* @deprecated: `author`, will be removed in Ghost 3.0
|
2018-03-27 17:16:15 +03:00
|
|
|
*/
|
2014-07-18 12:48:48 +04:00
|
|
|
if (object.posts[0].hasOwnProperty('author')) {
|
|
|
|
object.posts[0].author_id = object.posts[0].author;
|
|
|
|
delete object.posts[0].author;
|
|
|
|
}
|
2018-03-27 17:16:15 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensure correct incoming `post.authors` structure.
|
|
|
|
*
|
|
|
|
* NOTE:
|
2018-08-03 16:14:38 +03:00
|
|
|
* The `post.authors[*].id` attribute is required till we release Ghost 3.0.
|
2018-03-27 17:16:15 +03:00
|
|
|
* Ghost 1.x keeps the deprecated support for `post.author_id`, which is the primary author id and needs to be
|
|
|
|
* updated if the order of the `post.authors` array changes.
|
|
|
|
* If we allow adding authors via the post endpoint e.g. `authors=[{name: 'newuser']` (no id property), it's hard
|
|
|
|
* to update the primary author id (`post.author_id`), because the new author `id` is generated when attaching
|
|
|
|
* the author to the post. And the attach operation happens in bookshelf-relations, which happens after
|
|
|
|
* the event handling in the post model.
|
|
|
|
*
|
|
|
|
* It's solvable, but not worth right now solving, because the admin UI does not support this feature.
|
|
|
|
*
|
|
|
|
* TLDR; You can only attach existing authors to a post.
|
|
|
|
*
|
2018-08-03 16:14:38 +03:00
|
|
|
* @TODO: remove `id` restriction in Ghost 3.0
|
2018-03-27 17:16:15 +03:00
|
|
|
*/
|
|
|
|
if (object.posts[0].hasOwnProperty('authors')) {
|
|
|
|
if (!_.isArray(object.posts[0].authors) ||
|
|
|
|
(object.posts[0].authors.length && _.filter(object.posts[0].authors, 'id').length !== object.posts[0].authors.length)) {
|
|
|
|
return Promise.reject(new common.errors.BadRequestError({
|
|
|
|
message: common.i18n.t('errors.api.utils.invalidStructure', {key: 'posts[*].authors'})
|
|
|
|
}));
|
|
|
|
}
|
2018-04-05 16:59:52 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* CASE: we don't support updating nested-nested relations e.g. `post.authors[*].roles` yet.
|
|
|
|
*
|
|
|
|
* Bookshelf-relations supports this feature, BUT bookshelf's `hasChanged` fn will currently
|
|
|
|
* clash with this, because `hasChanged` won't be able to tell if relations have changed or not.
|
|
|
|
* It would always return `changed.roles = [....]`. It would always throw a model event that relations
|
|
|
|
* were updated, which is not true.
|
|
|
|
*
|
|
|
|
* Bookshelf-relations can tell us if a relation has changed, it knows that.
|
|
|
|
* But the connection between our model layer, Bookshelf's `hasChanged` fn and Bookshelf-relations
|
|
|
|
* is not present. As long as we don't support this case, we have to ignore this.
|
|
|
|
*/
|
|
|
|
if (object.posts[0].authors && object.posts[0].authors.length) {
|
|
|
|
_.each(object.posts[0].authors, (author, index) => {
|
|
|
|
if (author.hasOwnProperty('roles')) {
|
|
|
|
delete object.posts[0].authors[index].roles;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (author.hasOwnProperty('permissions')) {
|
|
|
|
delete object.posts[0].authors[index].permissions;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Model notation is: `tag.parent_id`.
|
|
|
|
* The API notation is `tag.parent`.
|
|
|
|
*
|
|
|
|
* See @TODO on the fn description. This information lives in two places. Not nice.
|
|
|
|
*/
|
|
|
|
if (object.posts[0].hasOwnProperty('tags')) {
|
|
|
|
if (_.isArray(object.posts[0].tags) && object.posts[0].tags.length) {
|
|
|
|
_.each(object.posts[0].tags, (tag, index) => {
|
|
|
|
if (tag.hasOwnProperty('parent')) {
|
|
|
|
object.posts[0].tags[index].parent_id = tag.parent;
|
|
|
|
delete object.posts[0].tags[index].parent;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tag.hasOwnProperty('posts')) {
|
|
|
|
delete object.posts[0].tags[index].posts;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-03-27 17:16:15 +03:00
|
|
|
}
|
2014-07-18 12:48:48 +04:00
|
|
|
}
|
2015-02-26 21:29:53 +03:00
|
|
|
|
2016-05-08 10:16:24 +03:00
|
|
|
// will remove unwanted null values
|
2018-09-18 16:59:56 +03:00
|
|
|
_.each(object[docName], (value, index) => {
|
2016-05-08 10:16:24 +03:00
|
|
|
if (!_.isObject(object[docName][index])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-06-11 21:23:27 +03:00
|
|
|
object[docName][index] = _.omitBy(object[docName][index], _.isNull);
|
2016-05-08 10:16:24 +03:00
|
|
|
});
|
|
|
|
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
if (editId && object[docName][0].id && editId !== object[docName][0].id) {
|
2017-12-12 00:47:46 +03:00
|
|
|
return Promise.reject(new common.errors.BadRequestError({
|
|
|
|
message: common.i18n.t('errors.api.utils.invalidIdProvided')
|
2016-10-06 15:27:35 +03:00
|
|
|
}));
|
2015-02-26 21:29:53 +03:00
|
|
|
}
|
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.resolve(object);
|
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
2014-05-08 16:41:19 +04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
module.exports = utils;
|