Ghost/core/server/data/schema/validator.js
Hannah Wolfe 1688b17c49
Refactored + cleaned up validation tools
- General code cleanup
- Removed unused notContains rule
- Swapped custom empty rule for builtin isEmpty rule
- Dropped usage of .extend on validator, as this was removed 2 years ago!
   - This will allow us to upgrade the validator dependency to a much newer version
- Changed our internal validator module to only expose the functions we use.
   - This gives us a clearer Public API
   - It makes it easier to see if we are affected by changes in validator
   - It's still easy to add another validator, we just have to update what we require
   - We can potentially use this to make smaller builds esp for client-side usage
   - Once ripped out into a module we can use ES imports :D
- Rejigged and _slightly_ improved the tests
2021-06-15 15:17:20 +01:00

128 lines
5.1 KiB
JavaScript

const _ = require('lodash');
const Promise = require('bluebird');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const {validator, validate} = require('../validation');
const schema = require('./schema');
const messages = {
valueCannotBeBlank: 'Value in [{tableName}.{columnKey}] cannot be blank.',
valueMustBeBoolean: 'Value in [{tableName}.{columnKey}] must be one of true, false, 0 or 1.',
valueExceedsMaxLength: 'Value in [{tableName}.{columnKey}] exceeds maximum length of {maxlength} characters.',
valueIsNotInteger: 'Value in [{tableName}.{columnKey}] is not an integer.'
};
/**
* Validate model against schema.
*
* ## on model update
* - only validate changed fields
* - otherwise we could throw errors which the user is out of control
* - e.g.
* - we add a new field without proper validation, release goes out
* - we add proper validation for a single field
* - if you call `user.save()` the default fallback in bookshelf is `options.method=update`.
* - we set `options.method` explicit for adding resources (because otherwise bookshelf uses `update`)
*
* ## on model add
* - validate everything to catch required fields
*/
function validateSchema(tableName, model, options) {
options = options || {};
const columns = _.keys(schema[tableName]);
let validationErrors = [];
_.each(columns, function each(columnKey) {
let message = ''; // KEEP: Validator.js only validates strings.
const strVal = _.toString(model.get(columnKey));
if (options.method !== 'insert' && !_.has(model.changed, columnKey)) {
return;
}
// check nullable
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'nullable') &&
schema[tableName][columnKey].nullable !== true &&
!Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo')
) {
if (validator.isEmpty(strVal)) {
message = tpl(messages.valueCannotBeBlank, {
tableName: tableName,
columnKey: columnKey
});
validationErrors.push(new errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
}
// validate boolean columns
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
&& schema[tableName][columnKey].type === 'bool') {
if (!(validator.isBoolean(strVal) || validator.isEmpty(strVal))) {
message = tpl(messages.valueMustBeBoolean, {
tableName: tableName,
columnKey: columnKey
});
validationErrors.push(new errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
// CASE: ensure we transform 0|1 to false|true
if (!validator.isEmpty(strVal)) {
model.set(columnKey, !!model.get(columnKey));
}
}
// TODO: check if mandatory values should be enforced
if (model.get(columnKey) !== null && model.get(columnKey) !== undefined) {
// check length
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'maxlength')) {
if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) {
message = tpl(messages.valueExceedsMaxLength,
{
tableName: tableName,
columnKey: columnKey,
maxlength: schema[tableName][columnKey].maxlength
});
validationErrors.push(new errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
}
// check validations objects
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'validations')) {
validationErrors = validationErrors.concat(validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName));
}
// check type
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')) {
if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) {
message = tpl(messages.valueIsNotInteger, {
tableName: tableName,
columnKey: columnKey
});
validationErrors.push(new errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
}
}
});
if (validationErrors.length !== 0) {
return Promise.reject(validationErrors);
}
return Promise.resolve();
}
module.exports = validateSchema;