import { IDtoVerify } from "../define/types" import { IDtoDefine } from "../define/types" import { VerifyDefineMap } from "./lib/VerifyDefineMap" import { useDtoDefine } from "./lib/useDtoDefine" import { isPlainObject } from "../../../is" export interface IDtoVerifyResult { isOk: boolean hasUnknownKey?: boolean errors?: { key: string; paths: string[]; msg?: string }[] msg?: string } /** * 验证数据是否符合 DTO 定义 */ export function dtoVerify( dtoDefine: any, data: any, options?: { allowUnknownKey?: boolean } ) { dtoDefine = useDtoDefine(dtoDefine) let result = verifiyObject(data, dtoDefine, { paths: [], ...(options || {}), }) return resultToReport(result) } function verifiyObject( data: any, dtoDefines: Record, options: { /** 遍历的路径 */ paths: string[] /** 允许未知键名 */ allowUnknownKey?: boolean } ): IDtoVerifyResult { if (data !== undefined && data !== null && typeof data !== "object") { return { isOk: false, msg: `!= object (received:${typeof data})`, } } let isOk = true let errors: { key: string; paths: string[]; msg?: string }[] = [] let hasUnknownKey = false for (const key in dtoDefines) { let dtoDefine = (dtoDefines as any)[key] as IDtoDefine let val = data ? (data as any)[key] : undefined // 处理字段是 class 函数的情况 // 如果字段是 class 函数,则实例化后再进行验证 if (typeof dtoDefine === "function" && (dtoDefine as any)?.prototype?.constructor) { try { dtoDefine = new (dtoDefine as any)() } catch (e) { // ignore } dtoDefine = useDtoDefine(dtoDefine) } // 字段如果是 Dto 定义 if (dtoDefine?.__isDto__) { if (dtoDefine.isOptional && val === undefined) { continue } if (dtoDefine.isNullable && val === null) { continue } let re = verifyBase(val, dtoDefine, { paths: options.paths, allowUnknownKey: options.allowUnknownKey }) if (re.isOk === false) { isOk = false errors.push({ key, paths: options.paths, msg: re.msg }) break } } let isArrayDto = dtoDefine?.__isDto__ && dtoDefine?.list && dtoDefine.type !== "or" if (isArrayDto || Array.isArray(dtoDefine)) { let list = isArrayDto ? dtoDefine.list : (dtoDefine as any) let re = verifyArray(val, list, { paths: [...options.paths, key], allowUnknownKey: options.allowUnknownKey, }) if (re.isOk === false) { isOk = false if (re.errors?.length) { errors.push(...re.errors) } else { errors.push({ key, paths: options.paths, msg: re.msg }) } if (re.hasUnknownKey) { hasUnknownKey = true } break } } let isObjectDto = dtoDefine?.__isDto__ && dtoDefine?.nest let isPlainObj = isPlainObject(dtoDefine) && !dtoDefine.__isDto__ if (isObjectDto || isPlainObj) { if (val === undefined || val === null) { if (isPlainObj) { isOk = false errors.push({ key, paths: options.paths, msg: `!= object (received:${val})` }) break } else { continue } } let nest = isObjectDto ? dtoDefine.nest : (dtoDefine as any) let re = verifiyObject(val, nest, { paths: [...options.paths, key], allowUnknownKey: options.allowUnknownKey, }) // console.log("nest verifiyObject:", { key, nest, dtoDefine }, re) if (re.isOk === false) { return re } } } if (isOk && !options.allowUnknownKey) { for (const key in data) { // console.log("dtoDefines:", { key, data, dtoDefines }) if (!(key in dtoDefines)) { isOk = false errors.push({ key, paths: options.paths, msg: `has unknown key: ${key}` }) hasUnknownKey = true break } } } return { isOk, errors, hasUnknownKey, } } function verifyBase( data: any, dtoDefine: IDtoDefine, options?: { paths: string[] allowUnknownKey?: boolean } ) { if (dtoDefine.type === "or" && dtoDefine.list) { for (let subDto of dtoDefine.list) { let re = verifyArrayElement(data, subDto, { paths: options?.paths || [], allowUnknownKey: options?.allowUnknownKey, }) if (re.isOk) { return { isOk: true } } } // 生成联合类型的错误消息,如 "string|number" const orTypes = dtoDefine.list.map((d: IDtoDefine) => d.type || "object").join("|") return { isOk: false, msg: `!= ${orTypes} (received:${typeof data})` } } let verifyFunc = (VerifyDefineMap as any)[dtoDefine.type].verify as IDtoVerify let verifyRe = verifyFunc(data, dtoDefine, dtoDefine.meta) // 如果验证不通过,直接返回结果 if (!verifyRe.isOk) { // 只有当验证函数没有返回自定义消息时,才使用默认的类型错误信息 if (!verifyRe.msg) { let expectedType: string = dtoDefine.type if (dtoDefine.isNullable) { expectedType = `${expectedType} | null` } verifyRe.msg = `!= ${expectedType} (received:${typeof data})` } return verifyRe } // 处理自定义 is 函数 if (dtoDefine.meta?.is) { if (Array.isArray(dtoDefine.meta.is)) { for (let isFunc of dtoDefine.meta.is) { let re = isFunc(data) if (re?.isOk === false) { return re } } } else { let re = dtoDefine.meta.is(data) if (re?.isOk === false) { return re } } } return verifyRe } function verifyArray( data: any[], dtoDefines: IDtoDefine[], options: { /** 遍历的路径 */ paths: string[] /** 允许未知键名 */ allowUnknownKey?: boolean } ): IDtoVerifyResult { if (!Array.isArray(data)) { return { isOk: false, errors: [ { key: "", paths: options.paths, msg: `!= Array (received:${typeof data})`, }, ], } } let foundUnknownKey = false for (let i = 0; i < data.length; i++) { const item = data[i] const indexedPaths = getIndexedPaths(options.paths, i) let matched = false let bestError: IDtoVerifyResult | undefined let bestErrorHasDetails = false for (let dtoDefine of dtoDefines) { // 处理字段是 class 函数的情况 // 如果字段是 class 函数,则实例化后再进行验证 if (typeof dtoDefine === "function" && (dtoDefine as any)?.prototype?.constructor) { try { dtoDefine = new (dtoDefine as any)() } catch (e) { // ignore } dtoDefine = useDtoDefine(dtoDefine) } const re = verifyArrayElement(item, dtoDefine, { paths: indexedPaths, allowUnknownKey: options.allowUnknownKey, }) if (re.isOk) { matched = true break } if (re?.hasUnknownKey) { foundUnknownKey = true } const hasDetails = Array.isArray(re.errors) && re.errors.length > 0 if (!bestError || (hasDetails && !bestErrorHasDetails)) { bestError = re bestErrorHasDetails = hasDetails } } if (!matched) { if (bestError?.errors?.length) { return { ...bestError, hasUnknownKey: foundUnknownKey } } const expectTypes = dtoDefines.map((d) => d.type || "object").join(" | ") const msg = bestErrorHasDetails && bestError?.msg ? bestError.msg : `!= ${expectTypes} (received:${typeof item})` return { isOk: false, errors: [ { key: "", paths: indexedPaths, msg, }, ], hasUnknownKey: foundUnknownKey, } } } return { isOk: true } } function verifyArrayElement( data: any, dtoDefine: IDtoDefine, options: { paths: string[] allowUnknownKey?: boolean } ): IDtoVerifyResult { if (isPlainObject(dtoDefine) && !dtoDefine.__isDto__) { return verifiyObject(data, dtoDefine as unknown as Record, { paths: options.paths, allowUnknownKey: options.allowUnknownKey, }) } let baseResult = verifyBase(data, dtoDefine, options) if (!baseResult.isOk) { return baseResult } if (dtoDefine?.__isDto__ && dtoDefine?.list && dtoDefine.type !== "or") { return verifyArray(data, dtoDefine.list, { paths: options.paths, allowUnknownKey: options.allowUnknownKey, }) } if (dtoDefine?.__isDto__ && dtoDefine?.nest) { return verifiyObject(data, dtoDefine.nest, { paths: options.paths, allowUnknownKey: options.allowUnknownKey, }) } return { isOk: true } } function getIndexedPaths(paths: string[], index: number) { if (!paths.length) { return [`[${index}]`] } const newPaths = [...paths] const last = newPaths.pop() if (last === undefined) { return [`[${index}]`] } newPaths.push(`${last}[${index}]`) return newPaths } function resultToReport(result: IDtoVerifyResult) { return { isOk: result.isOk, msgs: result?.errors?.map((error) => { let paths = error.key ? [...error.paths, error.key] : error.paths if (error?.msg?.[0] === "[") { return `.${paths.join(".")}${error.msg}`.replace("]", "]:") } else { return `.${paths.join(".")}: ${error.msg}` } }), } }