feat: enhance parameter validation (#6878)
* feat: support min/max items validation * feat: validate array deep and unique items * feat: validate object deep
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
in `./helpers` if you have the time.
|
||||
*/
|
||||
|
||||
import Im from "immutable"
|
||||
import Im, { fromJS, Set } from "immutable"
|
||||
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import upperFirst from "lodash/upperFirst"
|
||||
@@ -385,6 +385,40 @@ export const validateMaxLength = (val, max) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const validateUniqueItems = (val, uniqueItems) => {
|
||||
if (!val) {
|
||||
return
|
||||
}
|
||||
if (uniqueItems === "true" || uniqueItems === true) {
|
||||
const list = fromJS(val)
|
||||
const set = list.toSet()
|
||||
const hasDuplicates = val.length > set.size
|
||||
if(hasDuplicates) {
|
||||
let errorsPerIndex = Set()
|
||||
list.forEach((item, i) => {
|
||||
if(list.filter(v => isFunc(v.equals) ? v.equals(item) : v === item).size > 1) {
|
||||
errorsPerIndex = errorsPerIndex.add(i)
|
||||
}
|
||||
})
|
||||
if(errorsPerIndex.size !== 0) {
|
||||
return errorsPerIndex.map(i => ({index: i, error: "No duplicates allowed."})).toArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const validateMinItems = (val, min) => {
|
||||
if (!val && min >= 1 || val && val.length < min) {
|
||||
return `Array must contain at least ${min} item${min === 1 ? "" : "s"}`
|
||||
}
|
||||
}
|
||||
|
||||
export const validateMaxItems = (val, max) => {
|
||||
if (val && val.length > max) {
|
||||
return `Array must not contain more then ${max} item${max === 1 ? "" : "s"}`
|
||||
}
|
||||
}
|
||||
|
||||
export const validateMinLength = (val, min) => {
|
||||
if (val.length < min) {
|
||||
return `Value must be at least ${min} character${min !== 1 ? "s" : ""}`
|
||||
@@ -398,32 +432,28 @@ export const validatePattern = (val, rxPattern) => {
|
||||
}
|
||||
}
|
||||
|
||||
// validation of parameters before execute
|
||||
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
|
||||
|
||||
function validateValueBySchema(value, schema, isParamRequired, bypassRequiredCheck, parameterContentMediaType) {
|
||||
if(!schema) return []
|
||||
let errors = []
|
||||
|
||||
let paramRequired = param.get("required")
|
||||
|
||||
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
|
||||
|
||||
if(!paramDetails) return errors
|
||||
|
||||
let required = paramDetails.get("required")
|
||||
let maximum = paramDetails.get("maximum")
|
||||
let minimum = paramDetails.get("minimum")
|
||||
let type = paramDetails.get("type")
|
||||
let format = paramDetails.get("format")
|
||||
let maxLength = paramDetails.get("maxLength")
|
||||
let minLength = paramDetails.get("minLength")
|
||||
let pattern = paramDetails.get("pattern")
|
||||
let required = schema.get("required")
|
||||
let maximum = schema.get("maximum")
|
||||
let minimum = schema.get("minimum")
|
||||
let type = schema.get("type")
|
||||
let format = schema.get("format")
|
||||
let maxLength = schema.get("maxLength")
|
||||
let minLength = schema.get("minLength")
|
||||
let uniqueItems = schema.get("uniqueItems")
|
||||
let maxItems = schema.get("maxItems")
|
||||
let minItems = schema.get("minItems")
|
||||
let pattern = schema.get("pattern")
|
||||
|
||||
/*
|
||||
If the parameter is required OR the parameter has a value (meaning optional, but filled in)
|
||||
then we should do our validation routine.
|
||||
Only bother validating the parameter if the type was specified.
|
||||
in case of array an empty value needs validation too because constrains can be set to require minItems
|
||||
*/
|
||||
if ( type && (paramRequired || required || value) ) {
|
||||
if (type && (isParamRequired || required || value !== undefined || type === "array")) {
|
||||
// These checks should evaluate to true if there is a parameter
|
||||
let stringCheck = type === "string" && value
|
||||
let arrayCheck = type === "array" && Array.isArray(value) && value.length
|
||||
@@ -443,22 +473,37 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
|
||||
const passedAnyCheck = allChecks.some(v => !!v)
|
||||
|
||||
if ((paramRequired || required) && !passedAnyCheck && !bypassRequiredCheck ) {
|
||||
if ((isParamRequired || required) && !passedAnyCheck && !bypassRequiredCheck) {
|
||||
errors.push("Required field is not provided")
|
||||
return errors
|
||||
}
|
||||
|
||||
if (
|
||||
type === "object" &&
|
||||
typeof value === "string" &&
|
||||
(parameterContentMediaType === null ||
|
||||
parameterContentMediaType === "application/json")
|
||||
) {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
} catch (e) {
|
||||
errors.push("Parameter string value must be valid JSON")
|
||||
return errors
|
||||
let objectVal = value
|
||||
if(typeof value === "string") {
|
||||
try {
|
||||
objectVal = JSON.parse(value)
|
||||
} catch (e) {
|
||||
errors.push("Parameter string value must be valid JSON")
|
||||
return errors
|
||||
}
|
||||
}
|
||||
if(schema && schema.has("required") && isFunc(required.isList) && required.isList()) {
|
||||
required.forEach(key => {
|
||||
if(objectVal[key] === undefined) {
|
||||
errors.push({ propKey: key, error: "Required property not found" })
|
||||
}
|
||||
})
|
||||
}
|
||||
if(schema && schema.has("properties")) {
|
||||
schema.get("properties").forEach((val, key) => {
|
||||
const errs = validateValueBySchema(objectVal[key], val, false, bypassRequiredCheck, parameterContentMediaType)
|
||||
errors.push(...errs
|
||||
.map((error) => ({ propKey: key, error })))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +512,27 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
if (err) errors.push(err)
|
||||
}
|
||||
|
||||
if (minItems) {
|
||||
if (type === "array") {
|
||||
let err = validateMinItems(value, minItems)
|
||||
if (err) errors.push(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (maxItems) {
|
||||
if (type === "array") {
|
||||
let err = validateMaxItems(value, maxItems)
|
||||
if (err) errors.push({ needRemove: true, error: err })
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueItems) {
|
||||
if (type === "array") {
|
||||
let errorPerItem = validateUniqueItems(value, uniqueItems)
|
||||
if (errorPerItem) errors.push(...errorPerItem)
|
||||
}
|
||||
}
|
||||
|
||||
if (maxLength || maxLength === 0) {
|
||||
let err = validateMaxLength(value, maxLength)
|
||||
if (err) errors.push(err)
|
||||
@@ -487,52 +553,41 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
if (err) errors.push(err)
|
||||
}
|
||||
|
||||
if ( type === "string" ) {
|
||||
if (type === "string") {
|
||||
let err
|
||||
if (format === "date-time") {
|
||||
err = validateDateTime(value)
|
||||
err = validateDateTime(value)
|
||||
} else if (format === "uuid") {
|
||||
err = validateGuid(value)
|
||||
err = validateGuid(value)
|
||||
} else {
|
||||
err = validateString(value)
|
||||
err = validateString(value)
|
||||
}
|
||||
if (!err) return errors
|
||||
errors.push(err)
|
||||
} else if ( type === "boolean" ) {
|
||||
} else if (type === "boolean") {
|
||||
let err = validateBoolean(value)
|
||||
if (!err) return errors
|
||||
errors.push(err)
|
||||
} else if ( type === "number" ) {
|
||||
} else if (type === "number") {
|
||||
let err = validateNumber(value)
|
||||
if (!err) return errors
|
||||
errors.push(err)
|
||||
} else if ( type === "integer" ) {
|
||||
} else if (type === "integer") {
|
||||
let err = validateInteger(value)
|
||||
if (!err) return errors
|
||||
errors.push(err)
|
||||
} else if ( type === "array" ) {
|
||||
let itemType
|
||||
|
||||
if ( !arrayListCheck || !value.count() ) { return errors }
|
||||
|
||||
itemType = paramDetails.getIn(["items", "type"])
|
||||
|
||||
value.forEach((item, index) => {
|
||||
let err
|
||||
|
||||
if (itemType === "number") {
|
||||
err = validateNumber(item)
|
||||
} else if (itemType === "integer") {
|
||||
err = validateInteger(item)
|
||||
} else if (itemType === "string") {
|
||||
err = validateString(item)
|
||||
}
|
||||
|
||||
if ( err ) {
|
||||
errors.push({ index: index, error: err})
|
||||
}
|
||||
})
|
||||
} else if ( type === "file" ) {
|
||||
} else if (type === "array") {
|
||||
if (!(arrayCheck || arrayListCheck)) {
|
||||
return errors
|
||||
}
|
||||
if(value) {
|
||||
value.forEach((item, i) => {
|
||||
const errs = validateValueBySchema(item, schema.get("items"), false, bypassRequiredCheck, parameterContentMediaType)
|
||||
errors.push(...errs
|
||||
.map((err) => ({ index: i, error: err })))
|
||||
})
|
||||
}
|
||||
} else if (type === "file") {
|
||||
let err = validateFile(value)
|
||||
if (!err) return errors
|
||||
errors.push(err)
|
||||
@@ -542,6 +597,16 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
return errors
|
||||
}
|
||||
|
||||
// validation of parameters before execute
|
||||
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
|
||||
|
||||
let paramRequired = param.get("required")
|
||||
|
||||
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
|
||||
|
||||
return validateValueBySchema(value, paramDetails, paramRequired, bypassRequiredCheck, parameterContentMediaType)
|
||||
}
|
||||
|
||||
const getXmlSampleSchema = (schema, config, exampleOverride) => {
|
||||
if (schema && (!schema.xml || !schema.xml.name)) {
|
||||
schema.xml = schema.xml || {}
|
||||
|
||||
Reference in New Issue
Block a user