1import Ajv from 'ajv'; 2import i18n from '../i18n'; 3import retrieveSchema from './retriev'; 4 5import { 6 isObject, deepEquals 7} from '../utils'; 8import { getUserErrOptions } from '../formUtils'; 9 10let ajv = createAjvInstance(); 11 12let formerCustomFormats = null; 13let formerMetaSchema = null; 14 15// 创建实例 16function createAjvInstance() { 17 const ajvInstance = new Ajv({ 18 errorDataPath: 'property', 19 allErrors: true, 20 multipleOfPrecision: 8, 21 schemaId: 'auto', 22 unknownFormats: 'ignore', 23 }); 24 25 // 添加base-64 format 26 ajvInstance.addFormat( 27 'data-url', 28 /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/ 29 ); 30 31 // 添加color format 32 ajvInstance.addFormat( 33 'color', 34 // eslint-disable-next-line max-len 35 /^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/ 36 ); 37 return ajvInstance; 38} 39 40/** 41 * 将错误输出从ajv转换为jsonschema使用的格式 42 * At some point, components should be updated to support ajv. 43 */ 44function transformAjvErrors(errors = []) { 45 if (errors === null) { 46 return []; 47 } 48 49 return errors.map((e) => { 50 const { 51 dataPath, keyword, message, params, schemaPath 52 } = e; 53 const property = `${dataPath}`; 54 55 // put data in expected format 56 return { 57 name: keyword, 58 property, 59 message, 60 params, // specific to ajv 61 stack: `${property} ${message}`.trim(), 62 schemaPath, 63 }; 64 }); 65} 66 67/** 68 * 通过 schema校验formData并返回错误信息 69 * @param formData 校验的数据 70 * @param schema 71 * @param transformErrors function - 转换错误, 如个性化的配置 72 * @param additionalMetaSchemas 数组 添加 ajv metaSchema 73 * @param customFormats 添加 ajv 自定义 formats 74 * @returns {{errors: ([]|{stack: string, schemaPath: *, name: *, property: string, message: *, params: *}[])}} 75 */ 76export function ajvValidateFormData({ 77 formData, 78 schema, 79 transformErrors, 80 additionalMetaSchemas = [], 81 customFormats = {} 82} = {}) { 83 const hasNewMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas); 84 const hasNewFormats = !deepEquals(formerCustomFormats, customFormats); 85 86 // 变更了 Meta或者调整了format配置重置新的实例 87 if (hasNewMetaSchemas || hasNewFormats) { 88 ajv = createAjvInstance(); 89 } 90 91 // 添加更多要验证的模式 92 if ( 93 additionalMetaSchemas 94 && hasNewMetaSchemas 95 && Array.isArray(additionalMetaSchemas) 96 ) { 97 ajv.addMetaSchema(additionalMetaSchemas); 98 formerMetaSchema = additionalMetaSchemas; 99 } 100 101 // 注册自定义的 formats - 没有变更只会注册一次 - 否则重新创建实例 102 if (customFormats && hasNewFormats && isObject(customFormats)) { 103 Object.keys(customFormats).forEach((formatName) => { 104 ajv.addFormat(formatName, customFormats[formatName]); 105 }); 106 107 formerCustomFormats = customFormats; 108 } 109 110 let validationError = null; 111 try { 112 ajv.validate(schema, formData); 113 } catch (err) { 114 validationError = err; 115 } 116 117 // ajv 默认多语言处理 118 i18n.getCurrentLocalize()(ajv.errors); 119 120 let errors = transformAjvErrors(ajv.errors); 121 122 // 清除错误 123 ajv.errors = null; 124 125 // 处理异常 126 const noProperMetaSchema = validationError 127 && validationError.message 128 && typeof validationError.message === 'string' 129 && validationError.message.includes('no schema with key or ref '); 130 131 if (noProperMetaSchema) { 132 errors = [ 133 ...errors, 134 { 135 stack: validationError.message, 136 }, 137 ]; 138 } 139 140 // 转换错误, 如传入自定义的错误 141 if (typeof transformErrors === 'function') { 142 errors = transformErrors(errors); 143 } 144 145 return { 146 errors 147 }; 148} 149 150// 校验formData 并转换错误信息 151export function validateFormDataAndTransformMsg({ 152 formData, 153 schema, 154 uiSchema, 155 transformErrors, 156 additionalMetaSchemas = [], 157 customFormats = {}, 158 errorSchema = {}, 159 required = false, 160 propPath = '', 161 isOnlyFirstError = true, // 只取第一条错误信息 162} = {}) { 163 // 是否过滤根节点错误 固定只能根 164 const filterRootNodeError = true; 165 166 // 校验required信息 isEmpty 校验 167 // 如果数组类型针对配置了 format 的特殊处理 168 const emptyArray = (schema.type === 'array' && Array.isArray(formData) && formData.length === 0); 169 const isEmpty = formData === undefined || emptyArray; 170 171 if (required) { 172 if (isEmpty) { 173 const requireErrObj = { 174 keyword: 'required', 175 params: { 176 missingProperty: propPath 177 } 178 }; 179 180 // 用户设置校验信息 181 const errSchemaMsg = getUserErrOptions({ 182 schema, 183 uiSchema, 184 errorSchema 185 }).required; 186 if (errSchemaMsg) { 187 requireErrObj.message = errSchemaMsg; 188 } else { 189 // 处理多语言require提示信息 (ajv 修改原引用) 190 i18n.getCurrentLocalize()([requireErrObj]); 191 } 192 return [requireErrObj]; 193 } 194 } else if (isEmpty && !emptyArray) { 195 // 非required 为空 校验通过 196 return []; 197 } 198 199 // 校验ajv错误信息 200 let ajvErrors = ajvValidateFormData({ 201 formData, 202 schema, 203 transformErrors, 204 additionalMetaSchemas, 205 customFormats, 206 }).errors; 207 208 // 过滤顶级错误 209 if (filterRootNodeError) { 210 ajvErrors = ajvErrors.filter( 211 item => (item.property === '' 212 && (!item.schemaPath.includes('#/anyOf/') && !item.schemaPath.includes('#/oneOf/'))) 213 || item.name === 'additionalProperties' 214 ); 215 } 216 217 const userErrOptions = getUserErrOptions({ 218 schema, 219 uiSchema, 220 errorSchema 221 }); 222 223 return (isOnlyFirstError && ajvErrors.length > 0 ? [ajvErrors[0]] : ajvErrors).reduce((preErrors, errorItem) => { 224 // 优先获取 errorSchema 配置 225 errorItem.message = userErrOptions[errorItem.name] !== undefined ? userErrOptions[errorItem.name] : errorItem.message; 226 preErrors.push(errorItem); 227 return preErrors; 228 }, []); 229} 230 231/** 232 * 根据模式验证数据,如果数据有效则返回true,否则返回* false。如果模式无效,那么这个函数将返回* false。 233 * @param schema 234 * @param data 235 * @returns {boolean|PromiseLike<any>} 236 */ 237export function isValid(schema, data) { 238 try { 239 return ajv.validate(schema, data); 240 } catch (e) { 241 return false; 242 } 243} 244 245// ajv valida 246export function ajvValid(schema, data) { 247 return ajv.validate(schema, data); 248} 249 250// 如果查找不到 251// return -1 252export function getMatchingIndex(formData, options, rootSchema, haveAllFields = false) { 253 // eslint-disable-next-line no-plusplus 254 for (let i = 0; i < options.length; i++) { 255 const option = retrieveSchema(options[i], rootSchema, formData); 256 257 // If the schema describes an object then we need to add slightly more 258 // strict matching to the schema, because unless the schema uses the 259 // "requires" keyword, an object will match the schema as long as it 260 // doesn't have matching keys with a conflicting type. To do this we use an 261 // "anyOf" with an array of requires. This augmentation expresses that the 262 // schema should match if any of the keys in the schema are present on the 263 // object and pass validation. 264 if (option.properties) { 265 // Create an "anyOf" schema that requires at least one of the keys in the 266 // "properties" object 267 const requiresAnyOf = { 268 // 如果后代节点存在 $ref 需要正常引用 269 ...(rootSchema.definitions ? { 270 definitions: rootSchema.definitions 271 } : {}), 272 anyOf: Object.keys(option.properties).map(key => ({ 273 required: [key], 274 })), 275 }; 276 277 let augmentedSchema; 278 279 // If the "anyOf" keyword already exists, wrap the augmentation in an "allOf" 280 if (option.anyOf) { 281 // Create a shallow clone of the option 282 const { ...shallowClone } = option; 283 284 if (!shallowClone.allOf) { 285 shallowClone.allOf = []; 286 } else { 287 // If "allOf" already exists, shallow clone the array 288 shallowClone.allOf = shallowClone.allOf.slice(); 289 } 290 291 shallowClone.allOf.push(requiresAnyOf); 292 293 augmentedSchema = shallowClone; 294 } else { 295 augmentedSchema = Object.assign({}, option, requiresAnyOf); 296 } 297 298 // Remove the "required" field as it's likely that not all fields have 299 // been filled in yet, which will mean that the schema is not valid 300 301 // 如果编辑回填数据的场景 可直接使用 required 判断 302 if (!haveAllFields) delete augmentedSchema.required; 303 304 305 if (isValid(augmentedSchema, formData)) { 306 return i; 307 } 308 } else if (isValid(options[i], formData)) { 309 return i; 310 } 311 } 312 313 // 尝试查找const 配置 314 if (options[0] && options[0].properties) { 315 const constProperty = Object.keys(options[0].properties).find(k => options[0].properties[k].const); 316 if (constProperty) { 317 // eslint-disable-next-line no-plusplus 318 for (let i = 0; i < options.length; i++) { 319 if ( 320 options[i].properties 321 && options[i].properties[constProperty] 322 && options[i].properties[constProperty].const === formData[constProperty]) { 323 return i; 324 } 325 } 326 } 327 } 328 return -1; 329} 330 331// oneOf anyOf 通过formData的值来找到当前匹配项索引 332export function getMatchingOption(formData, options, rootSchema, haveAllFields = false) { 333 const index = getMatchingIndex(formData, options, rootSchema, haveAllFields); 334 return index === -1 ? 0 : index; 335} 336