UNPKG

erest

Version:

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

448 lines (447 loc) 15.7 kB
"use strict"; /** * @file API Scheme * @author Yourtion Guo <yourtion@gmail.com> */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SUPPORT_METHOD = void 0; const node_assert_1 = require("node:assert"); const path_to_regexp_1 = require("path-to-regexp"); const zod_1 = require("zod"); const debug_1 = require("./debug"); const params_1 = require("./params"); const utils_1 = require("./utils"); exports.SUPPORT_METHOD = ["get", "post", "put", "delete", "patch"]; class API { key; pathTestRegExp; inited; options; /** * 构造函数 */ constructor(method, path, sourceFile, group, prefix) { (0, node_assert_1.strict)(typeof method === "string", "`method`必须是字符串类型"); (0, node_assert_1.strict)(exports.SUPPORT_METHOD.indexOf(method.toLowerCase()) !== -1, `\`method\`必须是以下请求方法中的一个:${exports.SUPPORT_METHOD}`); (0, node_assert_1.strict)(typeof path === "string", "`path`必须是字符串类型"); (0, node_assert_1.strict)(path[0] === "/", '`path`必须以"/"开头'); if (prefix) (0, node_assert_1.strict)(prefix[0] === "/", '`prefix`必须以"/"开头'); this.key = (0, utils_1.getSchemaKey)(method, path, prefix || group); this.options = { sourceFile, method: method.toLowerCase(), path, realPath: (0, utils_1.getRealPath)(this.key), examples: [], required: new Set(), requiredOneOf: [], beforeHooks: new Set(), middlewares: new Set(), query: {}, body: {}, params: {}, headers: {}, _allParams: new Map(), group: group || "", tested: false, }; this.pathTestRegExp = (0, path_to_regexp_1.pathToRegexp)(this.options.realPath); this.inited = false; (0, debug_1.api)("new: %s %s from %s", method, path, sourceFile.absolute); } static define(options, sourceFile, group, prefix) { const schema = new API(options.method, options.path, sourceFile, group, prefix); schema.title(options.title); const g = group || options.group; if (g) { schema.group(g); } if (options.description) { schema.description(options.description); } if (options.response) { schema.response(options.response); } if (options.body) { schema.body(options.body); } if (options.query) { schema.query(options.query); } if (options.params) { schema.params(options.params); } if (options.headers) { schema.headers(options.headers); } if (options.required) { schema.required(options.required); } if (options.requiredOneOf) { schema.requiredOneOf(options.requiredOneOf); } if (options.middlewares && options.middlewares.length > 0) { schema.middlewares(...options.middlewares); } if (options.before && options.before.length > 0) { schema.before(...options.before); } if (options.handler) { schema.register(options.handler); } if (options.mock) { schema.mock(options.mock); } return schema; } /** * 检查是否已经完成初始化,如果是则报错 */ checkInited() { if (this.inited) { throw new Error(`${this.key}已经完成初始化,不能再进行更改`); } } /** * 检查URL是否符合API规则 */ pathTest(method, path) { return this.options.method === method.toLowerCase() && this.pathTestRegExp.test(path); } /** * API标题 */ title(title) { this.checkInited(); (0, node_assert_1.strict)(typeof title === "string", "`title`必须是字符串类型"); this.options.title = title; return this; } /** * API描述 */ description(description) { this.checkInited(); (0, node_assert_1.strict)(typeof description === "string", "`description`必须是字符串类型"); this.options.description = description; return this; } /** * API分组 */ group(group) { this.checkInited(); (0, node_assert_1.strict)(typeof group === "string", "`group`必须是字符串类型"); this.options.group = group; return this; } addExample(example) { this.options.examples.push(example); } /** * API使用例子 */ example(example) { // this.checkInited(); (0, node_assert_1.strict)(example.input && typeof example.input === "object", "`input`必须是一个对象"); (0, node_assert_1.strict)(example.output && typeof example.output === "object", "`output`必须是一个对象"); this.addExample(example); return this; } /** * 输出结果对象 */ response(response) { // assert(typeof response === "object", "`schema`必须是一个对象"); this.options.response = response; return this; } /** * 输入参数 */ setParam(name, options, place) { this.checkInited(); (0, node_assert_1.strict)(typeof name === "string", "`name`必须是字符串类型"); (0, node_assert_1.strict)(place && ["query", "body", "params", "headers"].indexOf(place) > -1, '`place` 必须是 "query" "body", "params", "headers"'); (0, node_assert_1.strict)(name.indexOf(" ") === -1, "`name`不能包含空格"); (0, node_assert_1.strict)(name[0] !== "$", '`name`不能以"$"开头'); (0, node_assert_1.strict)(!(name in this.options._allParams), `参数 ${name} 已存在`); (0, node_assert_1.strict)(options && (typeof options === "string" || typeof options === "object")); this.options._allParams.set(name, options); this.options[place][name] = options; } /** * 输入参数 */ setParams(place, obj) { for (const [key, o] of Object.entries(obj)) { this.setParam(key, o, place); } } /** * 检测混合使用并设置 Zod Schema */ setZodSchema(place, schema) { this.checkInited(); // 检查是否已经有 ISchemaType 参数 const hasISchemaType = Object.keys(this.options[place]).length > 0; if (hasISchemaType) { throw new Error(`Cannot mix ISchemaType and Zod schema in ${place}. Please use either ISchemaType or Zod schema, not both.`); } // 设置对应的 Zod Schema const schemaKey = `${place}Schema`; this.options[schemaKey] = schema; } /** * 检测混合使用并设置 ISchemaType 参数 */ checkMixedUsage(place) { const schemaKey = `${place}Schema`; if (this.options[schemaKey]) { throw new Error(`Cannot mix ISchemaType and Zod schema in ${place}. Please use either ISchemaType or Zod schema, not both.`); } } /** * Body 参数 - 支持 ISchemaType 和原生 Zod Schema */ body(obj) { if ((0, params_1.isZodSchema)(obj)) { this.setZodSchema("body", obj); } else if ((0, params_1.isISchemaTypeRecord)(obj)) { this.checkMixedUsage("body"); this.setParams("body", obj); } else { throw new Error("Body parameter must be either ISchemaType record or Zod schema"); } return this; } /** * Query 参数 - 支持 ISchemaType 和原生 Zod Schema */ query(obj) { if ((0, params_1.isZodSchema)(obj)) { this.setZodSchema("query", obj); } else if ((0, params_1.isISchemaTypeRecord)(obj)) { this.checkMixedUsage("query"); this.setParams("query", obj); } else { throw new Error("Query parameter must be either ISchemaType record or Zod schema"); } return this; } /** * Param 参数 - 支持 ISchemaType 和原生 Zod Schema */ params(obj) { if ((0, params_1.isZodSchema)(obj)) { this.setZodSchema("params", obj); } else if ((0, params_1.isISchemaTypeRecord)(obj)) { this.checkMixedUsage("params"); this.setParams("params", obj); } else { throw new Error("Params parameter must be either ISchemaType record or Zod schema"); } return this; } /** * Headers 参数 - 支持 ISchemaType 和原生 Zod Schema */ headers(obj) { if ((0, params_1.isZodSchema)(obj)) { this.setZodSchema("headers", obj); } else if ((0, params_1.isISchemaTypeRecord)(obj)) { this.checkMixedUsage("headers"); this.setParams("headers", obj); } else { throw new Error("Headers parameter must be either ISchemaType record or Zod schema"); } return this; } /** * 必填参数 */ required(list) { this.checkInited(); for (const item of list) { (0, node_assert_1.strict)(typeof item === "string", "`name`必须是字符串类型"); this.options.required.add(item); } return this; } /** * 多选一必填参数 */ requiredOneOf(list) { this.checkInited(); if (list.length > 0) { for (const item of list) { (0, node_assert_1.strict)(typeof item === "string", "`name`必须是字符串类型"); } this.options.requiredOneOf.push(list); } return this; } /** * 中间件 */ middlewares(...list) { this.checkInited(); for (const mid of list) { (0, node_assert_1.strict)(typeof mid === "function", "中间件必须是Function类型"); this.options.middlewares.add(mid); } return this; } /** * 注册执行之前的钩子 */ before(...list) { this.checkInited(); for (const hook of list) { (0, node_assert_1.strict)(typeof hook === "function", "钩子名称必须是Function类型"); this.options.beforeHooks.add(hook); } return this; } /** * 注册处理函数 */ register(fn) { this.checkInited(); (0, node_assert_1.strict)(typeof fn === "function", "处理函数必须是一个函数类型"); this.options.handler = fn; return this; } /** * 注册强类型处理函数 (基于 zod schema) */ registerTyped(schemas, handler) { this.checkInited(); // 设置 zod schemas if (schemas.query) { this.options.querySchema = schemas.query; } if (schemas.body) { this.options.bodySchema = schemas.body; } if (schemas.params) { this.options.paramsSchema = schemas.params; } if (schemas.headers) { this.options.headersSchema = schemas.headers; } if (schemas.response) { this.options.responseSchema = schemas.response; } // 包装处理函数,添加类型验证 const wrappedHandler = async (req, res) => { try { const reqObj = req; const validatedReq = { query: schemas.query ? schemas.query.parse(reqObj.query || {}) : {}, body: schemas.body ? schemas.body.parse(reqObj.body || {}) : {}, params: schemas.params ? schemas.params.parse(reqObj.params || {}) : {}, headers: schemas.headers ? schemas.headers.parse(reqObj.headers || {}) : {}, }; const result = await handler(validatedReq, res); // 验证响应 if (schemas.response) { return schemas.response.parse(result); } return result; } catch (error) { if (error.name === "ZodError") { const zodError = error; throw new Error(`Validation failed: ${zodError.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")}`); } throw error; } }; this.options.handler = wrappedHandler; return this; } mock(data) { this.checkInited(); this.options.mock = data || {}; } init(parent) { this.checkInited(); (0, node_assert_1.strict)(this.options.group, `请为 API ${this.key} 选择一个分组`); (0, node_assert_1.strict)(this.options.group && this.options.group in parent.privateInfo.groups, `请先配置 ${this.options.group} 分组`); // 初始化时参数类型检查 for (const [name, options] of this.options._allParams.entries()) { const typeName = options.type; // 特殊类型验证 if (typeName === "ENUM") { if (!options.params || !Array.isArray(options.params)) { throw new Error("ENUM is require a params"); } } // 检查是否为基础类型或已注册的自定义类型 // 处理数组类型,如 'JsonSchema[]' const baseTypeName = typeName.endsWith("[]") ? typeName.slice(0, -2) : typeName; if (!parent.type.has(baseTypeName) && !parent.schema.has(baseTypeName)) { // 检查是否为内置的 zod 类型 const builtinTypes = [ "string", "number", "integer", "boolean", "date", "email", "url", "uuid", "array", "object", "any", "JSON", "ENUM", "IntArray", "Date", "Array", "Number", "String", "Boolean", "Integer", ]; if (!builtinTypes.includes(baseTypeName)) { throw new Error(`Unknown type: ${baseTypeName}. Please register this type first.`); } } if (options.required) { this.options.required.add(name); } } if (this.options.response) { if (typeof this.options.response === "string") { this.options.responseSchema = parent.schema.get(this.options.response); } else if (this.options.response instanceof zod_1.z.ZodType) { this.options.responseSchema = this.options.response; } else if (typeof this.options.response.type === "string") { this.options.responseSchema = this.options.response; } else { this.options.responseSchema = parent.schema.createZodSchema(this.options.response); } } if (this.options.mock && parent.privateInfo.mockHandler && !this.options.handler) { this.options.handler = parent.privateInfo.mockHandler(this.options.mock); } this.inited = true; } } exports.default = API;