erest
Version:
Easy to build api server depend on @leizm/web and express.
448 lines (447 loc) • 15.7 kB
JavaScript
"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;