UNPKG

erest

Version:

Easy to build api server depend on @leizm/web and express.

648 lines (647 loc) 27.8 kB
"use strict"; /** * @file API 参数检测 * 基于 zod 实现 * @author Yourtion Guo <yourtion@gmail.com> */ Object.defineProperty(exports, "__esModule", { value: true }); exports.zodTypeMap = void 0; exports.isZodSchema = isZodSchema; exports.isISchemaType = isISchemaType; exports.isISchemaTypeRecord = isISchemaTypeRecord; exports.createZodSchema = createZodSchema; exports.paramsChecker = paramsChecker; exports.schemaChecker = schemaChecker; exports.responseChecker = responseChecker; exports.apiParamsCheck = apiParamsCheck; const zod_1 = require("zod"); const debug_1 = require("./debug"); /** * 检测是否为 Zod Schema */ function isZodSchema(obj) { if (!obj || typeof obj !== "object") return false; const objTyped = obj; return "_def" in objTyped && !!objTyped._def && typeof objTyped.parse === "function"; } /** * 检测是否为 ISchemaType 对象 */ function isISchemaType(obj) { if (!obj || typeof obj !== "object") return false; return typeof obj.type === "string" && !isZodSchema(obj); } /** * 检测是否为 ISchemaType 对象的集合 */ function isISchemaTypeRecord(obj) { if (!obj || typeof obj !== "object" || isZodSchema(obj)) { return false; } return Object.values(obj).every((value) => isISchemaType(value)); } // 基础类型映射 exports.zodTypeMap = { string: zod_1.z.string(), String: zod_1.z.string(), TrimString: zod_1.z.string().transform((val) => val.trim()), number: zod_1.z.union([ zod_1.z.number(), zod_1.z.string().transform((val) => { const num = Number(val); if (Number.isNaN(num)) throw new Error("Invalid number"); return num; }), ]), Number: zod_1.z.union([ zod_1.z.number(), zod_1.z.string().transform((val) => { const num = Number(val); if (Number.isNaN(num)) throw new Error("Invalid number"); return num; }), ]), Integer: zod_1.z.union([ zod_1.z.number().int(), zod_1.z.string().transform((val) => { // 检查是否包含小数点 if (val.includes(".")) throw new Error("Invalid integer"); const num = parseInt(val, 10); if (Number.isNaN(num)) throw new Error("Invalid integer"); return num; }), ]), Float: zod_1.z.union([ zod_1.z.number(), zod_1.z.string().transform((val) => { const num = parseFloat(val); if (Number.isNaN(num)) throw new Error("Invalid float"); return num; }), ]), boolean: zod_1.z.union([ zod_1.z.boolean(), zod_1.z.string().transform((val) => { if (val === "true") return true; if (val === "false") return false; throw new Error("Invalid boolean"); }), ]), Boolean: zod_1.z.union([ zod_1.z.boolean(), zod_1.z.string().transform((val) => { if (val === "true") return true; if (val === "false") return false; throw new Error("Invalid boolean"); }), ]), date: zod_1.z.union([zod_1.z.date(), zod_1.z.string().transform((val) => new Date(val))]), Date: zod_1.z.union([zod_1.z.date(), zod_1.z.string().transform((val) => new Date(val))]), email: zod_1.z.string().email(), Email: zod_1.z.string().email(), url: zod_1.z.string().url(), URL: zod_1.z.string().url(), uuid: zod_1.z.string().uuid(), array: zod_1.z.array(zod_1.z.any()), Array: zod_1.z.array(zod_1.z.any()), Object: zod_1.z.any(), object: zod_1.z.object({}), any: zod_1.z.any(), Any: zod_1.z.any(), JSON: zod_1.z.any(), // JSON 类型需要特殊处理 JSONString: zod_1.z.string(), ENUM: zod_1.z.enum(["placeholder"]), // 占位符,实际使用时需要传入具体的枚举值 IntArray: zod_1.z.union([ zod_1.z.array(zod_1.z.number().int()), zod_1.z.string().transform((val) => { return val .split(",") .map((v) => parseInt(v.trim(), 10)) .sort((a, b) => a - b); }), ]), StringArray: zod_1.z.union([ zod_1.z.array(zod_1.z.string()), zod_1.z.string().transform((val) => { return val.split(",").map((v) => v.trim()); }), zod_1.z.array(zod_1.z.any()).transform((arr) => arr.map((v) => String(v))), ]), NullableString: zod_1.z.string().nullable(), NullableInteger: zod_1.z .union([ zod_1.z.number().int(), zod_1.z.string().transform((val) => { // 检查是否包含小数点 if (val.includes(".")) throw new Error("Invalid integer"); const num = parseInt(val, 10); if (Number.isNaN(num)) throw new Error("Invalid integer"); return num; }), ]) .nullable(), MongoIdString: zod_1.z.string().regex(/^[0-9a-fA-F]{24}$/), Domain: zod_1.z.string(), Alpha: zod_1.z.string().regex(/^[a-zA-Z]+$/), AlphaNumeric: zod_1.z.string().regex(/^[a-zA-Z0-9]+$/), Ascii: zod_1.z.string(), Base64: zod_1.z.string(), }; // 类型转换函数 function createZodSchema(typeInfo) { if (typeof typeInfo === "string") { return exports.zodTypeMap[typeInfo] || zod_1.z.any(); } let schema; // 处理特殊类型 if (typeInfo.type === "ENUM") { if (typeInfo.params && Array.isArray(typeInfo.params) && typeInfo.params.length > 0) { // 使用 z.union 来支持混合类型的枚举值(字符串和数字) const literals = typeInfo.params.map((value) => zod_1.z.literal(value)); schema = literals.length === 1 ? literals[0] : zod_1.z.union([literals[0], ...literals.slice(1)]); } else { throw new Error("ENUM type requires params"); } } else if (typeInfo.type === "Array" && typeInfo.params) { const itemSchema = typeof typeInfo.params === "string" ? createZodSchema({ type: typeInfo.params }) : createZodSchema(typeInfo.params); schema = zod_1.z.array(itemSchema); } else if (typeInfo.type === "JSON") { // JSON 类型特殊处理 schema = zod_1.z.any(); } else if (["Number", "Integer", "Float"].includes(typeInfo.type) && typeInfo.params && typeof typeInfo.params === "object") { // 数值类型特殊处理:当有 min/max 参数时,需要创建支持 min/max 的基础 schema const numericParams = typeInfo.params; let baseSchema; if (typeInfo.type === "Number") { baseSchema = zod_1.z.number(); } else if (typeInfo.type === "Integer") { baseSchema = zod_1.z.number().int(); } else if (typeInfo.type === "Float") { baseSchema = zod_1.z.number(); } else { baseSchema = zod_1.z.number(); } // 应用 min/max 约束 if (numericParams.min !== undefined) { baseSchema = baseSchema.min(numericParams.min); } if (numericParams.max !== undefined) { baseSchema = baseSchema.max(numericParams.max); } // 为了保持与原有 union 类型的兼容性,仍然支持字符串输入 schema = zod_1.z.union([ baseSchema, zod_1.z.string().transform((val) => { const num = typeInfo.type === "Integer" ? parseInt(val, 10) : Number(val); if (Number.isNaN(num)) throw new Error(`Invalid ${typeInfo.type.toLowerCase()}`); // 验证转换后的数值是否满足约束 const numericParams = typeInfo.params; if (numericParams.min !== undefined && num < numericParams.min) { throw new Error(`Value ${num} is below minimum ${numericParams.min}`); } if (numericParams.max !== undefined && num > numericParams.max) { throw new Error(`Value ${num} is above maximum ${numericParams.max}`); } return num; }), ]); } else { schema = exports.zodTypeMap[typeInfo.type] || zod_1.z.any(); } // 处理 format 属性 if (typeInfo.format && typeInfo.type === "string") { // 可以根据 format 添加额外的验证 // 这里保持原有行为,format 主要用于文档生成 } // 处理默认值 if (typeInfo.default !== undefined) { schema = schema.default(typeInfo.default); } return schema; } const schemaDebug = (0, debug_1.create)("params:schema"); const apiDebug = (0, debug_1.create)("params:api"); function paramsChecker(ctx, name, input, typeInfo) { const { error } = ctx.privateInfo; try { // Array 类型特殊处理 - 需要处理元素的format属性 if (typeInfo.type === "Array" && Array.isArray(input) && typeInfo.params) { const _elementTypeInfo = typeof typeInfo.params === "string" ? { type: typeInfo.params } : typeInfo.params; // 对所有数组元素类型进行特殊处理 const processedArray = input.map((item, index) => { const elementTypeInfo = typeof typeInfo.params === "string" ? { type: typeInfo.params } : typeInfo.params; return paramsChecker(ctx, `${name}[${index}]`, item, elementTypeInfo); }); return processedArray; } // JSON 类型特殊处理 if (typeInfo.type === "JSON") { if (typeof input === "string") { try { const parsed = JSON.parse(input); return typeInfo.format !== false ? parsed : input; } catch (_e) { throw error.invalidParameter(`'${name}' should be valid JSON`); } } return input; } // JSONString 类型特殊处理 if (typeInfo.type === "JSONString") { if (typeof input === "string") { return typeInfo.format !== false ? input.trim() : input; } return String(input); } // Boolean 类型的format处理 if (typeInfo.type === "Boolean" && typeInfo.format === false) { return String(input); } // TrimString 类型特殊处理 if (typeInfo.type === "TrimString") { if (typeInfo.format === false) { return input; } } // 数字类型的format处理 if (["Integer", "Float", "Number"].includes(typeInfo.type) && typeInfo.format === false) { return String(input); } // NullableInteger 类型的format处理 if (typeInfo.type === "NullableInteger" && typeInfo.format === false) { return String(input); } const schema = createZodSchema(typeInfo); const result = schema.parse(input); (0, debug_1.params)("paramsChecker: ", input, "success", result); return result; } catch (zodError) { (0, debug_1.params)("paramsChecker: ", input, "failed", zodError.message); // 如果错误已经是我们自定义的参数错误(比如来自数组元素验证),直接重新抛出 if (zodError.message?.includes("incorrect parameter")) { throw zodError; } // 处理 Zod transform 函数抛出的错误 if (zodError.message && typeof zodError.message === "string") { if (zodError.message.includes("Invalid integer") || zodError.message.includes("Invalid number") || zodError.message.includes("Invalid float")) { throw error.invalidParameter(`'${name}' should be valid ${typeInfo.type}`); } if (zodError.message.includes("Invalid boolean")) { throw error.invalidParameter(`'${name}' should be valid ${typeInfo.type}`); } } const zodErr = zodError; if (zodErr.issues && zodErr.issues.length > 0) { const err = zodErr.issues[0]; // ENUM 类型特殊错误消息 if (typeInfo.type === "ENUM" && typeInfo.params && (err.code === "invalid_enum_value" || err.code === "invalid_union")) { const enumParams = typeInfo.params; throw error.invalidParameter(`'${name}' should be valid ENUM with additional restrictions: ${enumParams.join(",")}`); } // Array 类型错误处理 if (typeInfo.type === "Array" && err.path && err.path.length > 0) { const pathStr = err.path.map((p) => `[${p}]`).join(""); const elementType = typeof typeInfo.params === "string" ? typeInfo.params : typeInfo.params?.type || "JSON"; throw error.invalidParameter(`'${name}${pathStr}' should be valid ${elementType}`); } } // 生成统一的错误消息格式 throw error.invalidParameter(`'${name}' should be valid ${typeInfo.type}`); } } function schemaChecker(ctx, data, schema, requiredOneOf = []) { const { error } = ctx.privateInfo; try { let zodSchema; // 检测是否为原生 Zod Schema if (isZodSchema(schema)) { zodSchema = schema; } else if (isISchemaTypeRecord(schema)) { // 将 Record<string, ISchemaType> 转换为 zod object schema const schemaFields = {}; for (const [key, typeInfo] of Object.entries(schema)) { // 创建基础schema,但不包含默认值(对于必填字段) let fieldSchema; if (typeInfo.type === "ENUM") { if (typeInfo.params && Array.isArray(typeInfo.params)) { fieldSchema = zod_1.z.enum(typeInfo.params); } else { throw new Error("ENUM type requires params"); } } else if (typeInfo.type === "Array" && typeInfo.params) { const itemSchema = typeof typeInfo.params === "string" ? createZodSchema({ type: typeInfo.params }) : createZodSchema(typeInfo.params); fieldSchema = zod_1.z.array(itemSchema); } else { fieldSchema = exports.zodTypeMap[typeInfo.type] || zod_1.z.any(); } // 处理数值类型的 min/max 参数 if (["Number", "Integer", "Float"].includes(typeInfo.type) && typeInfo.params && typeof typeInfo.params === "object") { // 为数值类型创建支持约束的基础 schema let baseSchema = typeInfo.type === "Integer" ? zod_1.z.number().int() : zod_1.z.number(); // 应用 min/max 约束 const numericParams = typeInfo.params; if (numericParams.min !== undefined) { baseSchema = baseSchema.min(numericParams.min); } if (numericParams.max !== undefined) { baseSchema = baseSchema.max(numericParams.max); } // 创建与原有 union 类型兼容的 schema fieldSchema = zod_1.z.union([ baseSchema, zod_1.z .string() .transform((val) => { const num = Number(val); if (Number.isNaN(num)) throw new Error("Invalid number"); if (typeInfo.type === "Integer" && !Number.isInteger(num)) { throw new Error("Invalid integer"); } return num; }) .pipe(baseSchema), ]); } // 处理默认值和可选字段 if (typeInfo.default !== undefined) { fieldSchema = fieldSchema.default(typeInfo.default); } if (!typeInfo.required) { fieldSchema = fieldSchema.optional(); } schemaFields[key] = fieldSchema; } zodSchema = zod_1.z.object(schemaFields); } else { throw new Error("Invalid schema type"); } const value = zodSchema.parse(data); // 可选参数检查 let req = requiredOneOf.length < 1; for (const name of requiredOneOf) { req = typeof value[name] !== "undefined"; schemaDebug("requiredOneOf : %s - %s", name, req); if (req) break; } if (!req) throw error.missingParameter(`one of ${requiredOneOf.join(", ")} is required`); return value; } catch (zodError) { const zodErr = zodError; if (zodErr.issues) { // 处理原生 Zod Schema 的错误 if (isZodSchema(schema)) { // 对于原生 Zod Schema,直接处理 Zod 错误 if (requiredOneOf.length > 0) { // 检查requiredOneOf中是否至少有一个字段存在 const hasRequiredOneOf = requiredOneOf.some((fieldName) => { return !zodErr.issues?.some((err) => err.code === "invalid_type" && err.received === undefined && err.path.join(".") === fieldName); }); if (!hasRequiredOneOf) { throw error.missingParameter(`one of ${requiredOneOf.join(", ")} is required`); } } // 处理原生 Zod Schema 的验证错误 for (const err of zodErr.issues || []) { const fieldName = err.path.join(".") || "value"; if (err.code === "invalid_type" && err.received === undefined) { throw error.missingParameter(`'${fieldName}'`); } else { // 根据 Zod 错误类型生成合适的错误消息 let errorMessage = `'${fieldName}' should be valid`; if (err.expected) { errorMessage += ` ${err.expected}`; } throw error.invalidParameter(errorMessage); } } } else { // 处理 ISchemaType 的错误(保持原有逻辑) const fieldSchema = schema; // 如果有requiredOneOf参数,检查是否满足条件 if (requiredOneOf.length > 0) { // 检查requiredOneOf中是否至少有一个字段存在 const hasRequiredOneOf = requiredOneOf.some((fieldName) => { return !(zodErr.issues || []).some((err) => err.code === "invalid_type" && err.received === undefined && err.path.join(".") === fieldName); }); if (!hasRequiredOneOf) { throw error.missingParameter(`one of ${requiredOneOf.join(", ")} is required`); } } else { // 如果没有requiredOneOf,优先检查必填字段缺失 for (const err of zodErr.issues || []) { let fieldName; let isUndefinedError = false; if (err.code === "invalid_type" && err.received === undefined) { fieldName = err.path.join("."); isUndefinedError = true; } else if (err.code === "invalid_union" && err.path.length > 0) { // 检查union错误中是否包含undefined类型错误 const hasUndefinedError = err.errors?.some((errorGroup) => Array.isArray(errorGroup) && errorGroup.some((e) => e.code === "invalid_type" && e.received === undefined)); if (hasUndefinedError) { fieldName = err.path.join("."); isUndefinedError = true; } } if (isUndefinedError && fieldName) { const fieldInfo = fieldSchema[fieldName]; if (fieldInfo?.required && fieldInfo.default === undefined) { throw error.missingParameter(`'${fieldName}'`); } } } } // 然后处理其他类型的错误 for (const err of zodErr.issues || []) { const fieldName = err.path.join("."); const fieldInfo = fieldSchema[fieldName]; // 跳过已经处理过的undefined错误 if ((err.code === "invalid_type" && err.received === undefined) || (err.code === "invalid_union" && err.errors?.some((errorGroup) => Array.isArray(errorGroup) && errorGroup.some((e) => e.code === "invalid_type" && e.received === undefined)))) { // 如果是缺失字段但不是必填字段(或有默认值),报告类型错误 if (!fieldInfo || !fieldInfo.required || fieldInfo.default !== undefined) { const fieldType = fieldInfo?.type || "unknown"; throw error.invalidParameter(`'${fieldName}' should be valid ${fieldType}`); } // 必填字段缺失的情况已经在上面处理了,这里不应该到达 } else { // 处理其他类型的错误(类型不匹配等) const fieldType = fieldInfo?.type || "unknown"; throw error.invalidParameter(`'${fieldName}' should be valid ${fieldType}`); } } } } // 处理 transform 函数抛出的错误 const zodErrWithMessage = zodError; if (zodErrWithMessage.message && typeof zodErrWithMessage.message === "string") { if (isZodSchema(schema)) { // 对于原生 Zod Schema,直接使用错误消息 throw error.invalidParameter(zodErrWithMessage.message); } else { // 尝试从错误消息中提取字段信息 const fieldNames = Object.keys(schema); for (const fieldName of fieldNames) { const fieldSchema = schema; const fieldType = fieldSchema[fieldName]?.type; if (fieldType && zodErrWithMessage.message.includes("Invalid")) { throw error.invalidParameter(`'${fieldName}' should be valid ${fieldType}`); } } } } throw error.invalidParameter(JSON.stringify(zodErr.issues || zodErrWithMessage.message)); } } function responseChecker(_ctx, data, schema) { try { let zodSchema; // 检测是否为原生 Zod Schema if (isZodSchema(schema)) { zodSchema = schema; } else if (isISchemaType(schema)) { zodSchema = createZodSchema(schema); } else if (isISchemaTypeRecord(schema)) { // 将 Record<string, ISchemaType> 转换为 zod object schema const schemaFields = {}; for (const [key, typeInfo] of Object.entries(schema)) { schemaFields[key] = createZodSchema(typeInfo); } zodSchema = zod_1.z.object(schemaFields); } else { throw new Error("Invalid schema type"); } const value = zodSchema.parse(data); return { ok: true, message: "success", value }; } catch (zodError) { // 响应验证失败时返回原始数据,避免破坏正常流程 (0, debug_1.params)("responseChecker failed:", zodError.message); return data; } } /** * API 参数检查 */ function apiParamsCheck(ctx, schema, params, query, body, headers) { const { error } = ctx.privateInfo; const newParams = {}; // 检查 params - 支持原生 Zod Schema 和 ISchemaType if (schema.options.paramsSchema && params) { const res = schemaChecker(ctx, params, schema.options.paramsSchema); Object.assign(newParams, res); } else if (schema.options.params && params && Object.keys(schema.options.params).length > 0) { const res = schemaChecker(ctx, params, schema.options.params); Object.assign(newParams, res); } // 检查 query - 支持原生 Zod Schema 和 ISchemaType if (schema.options.querySchema && query) { const res = schemaChecker(ctx, query, schema.options.querySchema); Object.assign(newParams, res); } else if (schema.options.query && query && Object.keys(schema.options.query).length > 0) { const res = schemaChecker(ctx, query, schema.options.query); Object.assign(newParams, res); } // 检查 body - 支持原生 Zod Schema 和 ISchemaType if (schema.options.bodySchema && body) { const res = schemaChecker(ctx, body, schema.options.bodySchema); Object.assign(newParams, res); } else if (schema.options.body && body && Object.keys(schema.options.body).length > 0) { const res = schemaChecker(ctx, body, schema.options.body); Object.assign(newParams, res); } // 检查 headers - 支持原生 Zod Schema 和 ISchemaType if (schema.options.headersSchema && headers) { const res = schemaChecker(ctx, headers, schema.options.headersSchema); Object.assign(newParams, res); } else if (schema.options.headers && headers && Object.keys(schema.options.headers).length > 0) { const res = schemaChecker(ctx, headers, schema.options.headers); Object.assign(newParams, res); } // 必填参数检查 if (schema.options.required.size > 0) { for (const name of schema.options.required) { apiDebug("required : %s", name); if (!(name in newParams)) throw error.missingParameter(`'${name}'`); } } // 可选参数检查 if (schema.options.requiredOneOf.length > 0) { for (const names of schema.options.requiredOneOf) { apiDebug("requiredOneOf : %o", names); let ok = false; for (const name of names) { ok = typeof newParams[name] !== "undefined"; apiDebug("requiredOneOf : %s - %s", name, ok); if (ok) { break; } } if (!ok) { throw error.missingParameter(`one of ${names.join(", ")} is required`); } } } return newParams; }