82c612bad9
- this updates a bunch of places where we're just using Object to cheat the system - doing this means editor autocomplete and basic type checking is better because we now have proper types in place - functionality should not change, these are just comments
229 lines
7.0 KiB
JavaScript
229 lines
7.0 KiB
JavaScript
const debug = require('@tryghost/debug')('validators:input:all');
|
|
const _ = require('lodash');
|
|
const tpl = require('@tryghost/tpl');
|
|
const {BadRequestError, ValidationError} = require('@tryghost/errors');
|
|
const validator = require('@tryghost/validator');
|
|
|
|
const messages = {
|
|
validationFailed: 'Validation ({validationName}) failed for {key}',
|
|
noRootKeyProvided: 'No root key (\'{docName}\') provided.',
|
|
invalidIdProvided: 'Invalid id provided.'
|
|
};
|
|
|
|
const GLOBAL_VALIDATORS = {
|
|
id: {matches: /^[a-f\d]{24}$|^1$|me/i},
|
|
page: {matches: /^\d+$/},
|
|
limit: {matches: /^\d+|all$/},
|
|
from: {isDate: true},
|
|
to: {isDate: true},
|
|
columns: {matches: /^[\w, ]+$/},
|
|
order: {matches: /^[a-z0-9_,. ]+$/i},
|
|
uuid: {isUUID: true},
|
|
slug: {isSlug: true},
|
|
name: {},
|
|
email: {isEmail: true},
|
|
filter: false,
|
|
context: false,
|
|
forUpdate: false,
|
|
transacting: false,
|
|
include: false,
|
|
formats: false
|
|
};
|
|
|
|
const validate = (config, attrs) => {
|
|
let errors = [];
|
|
|
|
_.each(config, (value, key) => {
|
|
if (value.required && !attrs[key]) {
|
|
errors.push(new ValidationError({
|
|
message: tpl(messages.validationFailed, {
|
|
validationName: 'FieldIsRequired',
|
|
key: key
|
|
})
|
|
}));
|
|
}
|
|
});
|
|
|
|
_.each(attrs, (value, key) => {
|
|
debug(key, value);
|
|
|
|
if (GLOBAL_VALIDATORS[key]) {
|
|
debug('global validation');
|
|
errors = errors.concat(validator.validate(value, key, GLOBAL_VALIDATORS[key]));
|
|
}
|
|
|
|
if (config?.[key]) {
|
|
const allowedValues = Array.isArray(config[key]) ? config[key] : config[key].values;
|
|
|
|
if (allowedValues) {
|
|
debug('ctrl validation');
|
|
|
|
// CASE: we allow e.g. `formats=`
|
|
if (!value || !value.length) {
|
|
return;
|
|
}
|
|
|
|
const valuesAsArray = Array.isArray(value) ? value : value.trim().toLowerCase().split(',');
|
|
const unallowedValues = _.filter(valuesAsArray, (valueToFilter) => {
|
|
return !allowedValues.includes(valueToFilter);
|
|
});
|
|
|
|
if (unallowedValues.length) {
|
|
// CASE: we do not error for invalid includes, just silently remove
|
|
if (key === 'include') {
|
|
attrs.include = valuesAsArray.filter(x => allowedValues.includes(x));
|
|
return;
|
|
}
|
|
|
|
errors.push(new ValidationError({
|
|
message: tpl(messages.validationFailed, {
|
|
validationName: 'AllowedValues',
|
|
key: key
|
|
})
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return errors;
|
|
};
|
|
|
|
module.exports = {
|
|
/**
|
|
* @param {object} apiConfig
|
|
* @param {import('@tryghost/api-framework').Frame} frame
|
|
*/
|
|
all(apiConfig, frame) {
|
|
debug('validate all');
|
|
|
|
let validationErrors = validate(apiConfig.options, frame.options);
|
|
|
|
if (!_.isEmpty(validationErrors)) {
|
|
return Promise.reject(validationErrors[0]);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
},
|
|
|
|
/**
|
|
* @param {object} apiConfig
|
|
* @param {import('@tryghost/api-framework').Frame} frame
|
|
*/
|
|
browse(apiConfig, frame) {
|
|
debug('validate browse');
|
|
|
|
let validationErrors = [];
|
|
|
|
if (frame.data) {
|
|
validationErrors = validate(apiConfig.data, frame.data);
|
|
}
|
|
|
|
if (!_.isEmpty(validationErrors)) {
|
|
return Promise.reject(validationErrors[0]);
|
|
}
|
|
},
|
|
|
|
read() {
|
|
debug('validate read');
|
|
return this.browse(...arguments);
|
|
},
|
|
|
|
/**
|
|
* @param {object} apiConfig
|
|
* @param {import('@tryghost/api-framework').Frame} frame
|
|
*/
|
|
add(apiConfig, frame) {
|
|
debug('validate add');
|
|
|
|
// NOTE: this block should be removed completely once JSON Schema validations
|
|
// are introduced for all of the endpoints
|
|
if (!['posts', 'tags'].includes(apiConfig.docName)) {
|
|
if (_.isEmpty(frame.data) || _.isEmpty(frame.data[apiConfig.docName]) || _.isEmpty(frame.data[apiConfig.docName][0])) {
|
|
return Promise.reject(new BadRequestError({
|
|
message: tpl(messages.noRootKeyProvided, {docName: apiConfig.docName})
|
|
}));
|
|
}
|
|
}
|
|
|
|
const jsonpath = require('jsonpath');
|
|
|
|
if (apiConfig.data) {
|
|
const missedDataProperties = [];
|
|
const nilDataProperties = [];
|
|
|
|
_.each(apiConfig.data, (value, key) => {
|
|
if (jsonpath.query(frame.data[apiConfig.docName][0], key).length === 0) {
|
|
missedDataProperties.push(key);
|
|
} else if (_.isNil(frame.data[apiConfig.docName][0][key])) {
|
|
nilDataProperties.push(key);
|
|
}
|
|
});
|
|
|
|
if (missedDataProperties.length) {
|
|
return Promise.reject(new ValidationError({
|
|
message: tpl(messages.validationFailed, {
|
|
validationName: 'FieldIsRequired',
|
|
key: JSON.stringify(missedDataProperties)
|
|
})
|
|
}));
|
|
}
|
|
|
|
if (nilDataProperties.length) {
|
|
return Promise.reject(new ValidationError({
|
|
message: tpl(messages.validationFailed, {
|
|
validationName: 'FieldIsInvalid',
|
|
key: JSON.stringify(nilDataProperties)
|
|
})
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {object} apiConfig
|
|
* @param {import('@tryghost/api-framework').Frame} frame
|
|
*/
|
|
edit(apiConfig, frame) {
|
|
debug('validate edit');
|
|
const result = this.add(...arguments);
|
|
|
|
if (result instanceof Promise) {
|
|
return result;
|
|
}
|
|
|
|
// NOTE: this block should be removed completely once JSON Schema validations
|
|
// are introduced for all of the endpoints. `id` property is currently
|
|
// stripped from the request body and only the one provided in `options`
|
|
// is used in later logic
|
|
if (!['posts', 'tags'].includes(apiConfig.docName)) {
|
|
if (frame.options.id && frame.data[apiConfig.docName][0].id
|
|
&& frame.options.id !== frame.data[apiConfig.docName][0].id) {
|
|
return Promise.reject(new BadRequestError({
|
|
message: tpl(messages.invalidIdProvided)
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
|
|
changePassword() {
|
|
debug('validate changePassword');
|
|
return this.add(...arguments);
|
|
},
|
|
|
|
resetPassword() {
|
|
debug('validate resetPassword');
|
|
return this.add(...arguments);
|
|
},
|
|
|
|
setup() {
|
|
debug('validate setup');
|
|
return this.add(...arguments);
|
|
},
|
|
|
|
publish() {
|
|
debug('validate schedule');
|
|
return this.browse(...arguments);
|
|
}
|
|
};
|