瀑布流式感应
“瀑布流式感应”(Service -> Controller -> Hooks)
1. 重新设计的“瀑布流”驱动模型
在这种模型下,Service 是逻辑的源头,下游自动感应。
1.扫描table.schema.ts 中的table ,带有@gen-skip 就跳过 不进行任何生成
在 table.schema.ts 中:
1/**
2 * @gen-skip
3 */
4export const userRoleTable = ... // 中间表,跳过
5
6// 无注解 → 默认参与生成(但方法由 manifest 决定)
7export const tenantTable = ...
-
第一级:Service (手动/模板驱动)
-
脚本读取
manifest,确定该实体需要哪些方法。 -
根据模板生成带
/** @generated */的 Service 方法。 -
你也可以手写不带该标记的 Service 方法。
-
-
第二级:Controller (Service 感应)
-
ControllerTask扫描对应的 Service 类。 -
只要 Service 里有一个
public方法(无论是自动生成的还是你手写的),Controller 就会尝试为其生成一个对应的 API 路由。 -
通过
// @generated标记来决定是否覆盖。
-
-
第三级:Hooks (Controller 全量感应)
-
FrontendHookTask扫描生成的 Controller 路由。 -
它不关心逻辑是怎么实现的,它只看 Controller 暴露了哪些
.get,.post路径。 -
即便你手写了一个复杂的路由,只要在 Controller 里,Hook 就能自动长出来。
-
2. 完美的增量更新:// @generated 哨兵模式
针对你最担心的 Elysia 链式调用和方法级同步,我们需要在代码生成逻辑中实现“原子化手术”。
Service 层(类方法级):
TypeScript
export class UserService {
/** @generated */
async list(ctx: ServiceContext) { ... } // 脚本会更新这里
async customLogic(data: any) { ... } // 脚本检测到没标记,直接跳过
}
Controller 层(链式调用级):
TypeScript
export const userController = new Elysia({ prefix: "/user" })
// @generated
.get("/", () => userService.list()) // 脚本检测到上方有标记,会根据 Service 的变化更新此处
.get("/me", () => { ... }) // 脚本检测到没有标记,绝对不会碰这块代码
// @generated
.post("/", () => userService.create())
3. 实现这一架构的“三步走”方案
为了达到这个目标,我们需要对目前的脚本进行重构:
步骤 A:增强 AST 扫描器 (scanner.ts)
-
引入
ts-morph深度扫描 Service 类的所有方法。 -
提取方法名、参数类型、返回类型,作为下一层的元数据。
步骤 B:实现 Elysia 路由注入器 (controller.task.ts)
-
不再使用全量覆盖,而是解析链式调用。
-
寻找带有
// @generated的.get/post/put调用节点。 -
如果 Service 新增了方法,则在链式调用末尾追加新的路由块。
步骤 C:Hooks 的全量路径扫描 (frontend-hook.task.ts)
-
遍历
user.controller.ts的所有CallExpression。 -
分析其 Method (get/post) 和 Path ("/", "/:id")。
-
无论路由来源,统一生成对应的 React Query Hook。
4. 最终的开发体验
-
想增加功能? 只需要在
autogen.config.ts的manifest里的service数组加一个@export。 -
运行
sync:-
userService多了一个export方法。 -
userController多了一个.post("/export")路由。 -
前端多了
useExportUserHook。
-
-
想手写特殊逻辑? 在 Controller 里直接写一个
.get("/complex"),不加@generated。再次运行sync时,后端逻辑不变,但前端会自动生成useComplexUserHook。
这个流程的“爽点”在于自动化,而“难点”在于如何让生成的代码既符合规范又能灵活扩展。要让 Controller 自动感应 Service 并生成正确的路由,我们需要一套**约定优于配置(Convention over Configuration)**的机制来推断请求类型、路径和中间件。
以下是针对你提出的“请求类型、路径、参数、中间件”如何自动推导的设计方案:
1. 约定式请求类型与路径推导
我们不需要在 Service 里写复杂的装饰器,而是通过 Service 方法名的前缀 结合 参数契约 来实现自动推导。
| Service 方法前缀 | 推导请求 (Method) | 推导路径 (Path) | 参数来源 | 说明 |
|---|---|---|---|---|
list / | GET | / | query | 列表 |
get* | GET | /:id | params | 获取单条详情 |
create / | POST | / | body | 新增记录 |
update | PUT | /:id | params + body | 全量更新 |
patch* | PATCH | /:id | params + body | 局部更新状态 |
delete | DELETE | /:id | params | 删除记录 |
| tree | GET | /tree | query | 树形列表 |
| pageList | GET | /page | query | 分页、过滤 |
自定义方法处理:
如果 Service 中出现不符合上述前缀的方法(如 syncData),则默认推导为 POST /sync-data,并接受 body 参数。
2. 参数类型的自动提取
利用 ts-morph 的 AST 解析能力,ControllerTask 在扫描 Service 时可以直接提取参数的类型信息。
-
识别逻辑:
-
如果方法的参数中包含
id: string,Controller 自动注入/:id路径并校验params。 -
如果参数中包含
query(对应 Contract 的ListQuery),Controller 自动注入query校验。 -
如果参数中包含
body(对应 Contract 的Create或Update),Controller 自动注入body校验。
-
示例:
若 Service 定义为 update(id: string, body: UserContract["Update"]),生成的 Controller 会自动拼装为:
TypeScript
.put("/:id", ({ params, body }) => userService.update(params.id, body), {
params: t.Object({ id: t.String() }),
body: UserContract.Update
})
3. 中间件与第三个参数的模板化
Elysia 的第三个参数(包含权限、Hook、中间件等)可以通过**“元配置 + 局部重载”**的方式实现。
A. 默认配置模板 (Manifest 定义)
在 autogen.config.ts 的 manifest 中,为每个实体或每个层级定义默认的配置模板:
TypeScript
manifest: {
user: {
service: ["@list", "@create"],
controller: {
middleware: "authGuardMid", // 默认中间件模板
requireDept: true // 默认业务开关
}
}
}
B. 细粒度控制 (JSDoc 覆盖)
如果某个特定方法需要不同的中间件,可以在 Service 的方法上通过特定的 JSDoc 标签来覆盖默认行为。
TypeScript
export class UserService {
/** * @route.middleware none
* @route.summary "公开获取列表"
* @generated
*/
async listPublic(query: any) { ... }
}
生成器在扫描到 @route.* 标签时,会优先使用这些值来填充 Controller 的第三个参数。
4. 瀑布流感应流程:从 Service 到 Hook 的全自动链路
-
Service 阶段:你定义了
async tree(ctx: ServiceContext)方法。 -
Controller 感应:
-
ControllerTask扫描到tree方法。 -
由于方法名包含
tree且无id参数,推导出GET /tree。 -
生成带
// @generated标记的代码块并插入 Controller。
-
-
Hooks 全量感应:
-
FrontendHookTask扫描 Controller。 -
识别到新的
.get("/tree")路由。 -
自动生成
useUserTree钩子,包含正确的 QueryKey 和 API 调用。
-
5. 核心优势:为什么这很“爽”?
-
零重复工作:你只需要关注 Service 里的业务逻辑,不需要再去 Controller 里手动复制一遍路径和权限。
-
强类型保障:通过自动扫描参数类型,Controller 的校验对象与 Service 的参数类型永远是 1:1 同步的。
-
手动/自动混排:通过
// @generated哨兵模式,你可以在 Controller 里保留手写的/me等特殊接口,而常规的 CRUD 随 Service 自动起舞。
// autogen.config.ts 里的 manifest 部分
manifest: {
"user": {
// 逻辑层:决定 Service 生成哪些原子方法
"service": ["@list", "@create", "@update", "@delete", "@customAction"],
// 表现层:控制 Controller 的行为
"controller": {
"gen": true, // 是否生成 Controller 文件
"autoRoute": true, // 是否根据 Service 方法自动映射路由
"auth": "authGuardMid" // 默认插件
},
// 客户端层:基于 Controller 结果的二次过滤
"hooks": {
"exclude": ["/internal-admin-only"], // 排除特定的路由不生成 Hook
}
}
}
2. “瀑布流式”生成逻辑:层层推导,而非底层决定
你提出的“前一层决定后一层”是极其高明的。我们不从 Table 直接推导出 Hook,而是建立如下依赖链:
-
Service 扫描 Service 模板:
ServiceTask扫描templates/service/*.ts。如果你在manifest里给User配置了@list,它就去找list.ts模板。 -
Controller 扫描 Service 实例:
ControllerTask不看 Table,它直接利用ts-morph扫描对应的UserService类。它发现 Service 有findAll方法,就自动生成对应的.get("/")路由。 -
Hook 扫描 Controller 路由:这是最关键的一步。
FrontendHookTask扫描user.controller.ts的所有链式调用(.get,.post等)。无论这个路由是自动生成的,还是你手写的,只要它存在于 Controller 中,Hook 就会自动长出来。
增强型 AST 更新:链式调用的“精准手术”
你提到的 Elysia 链式调用更新确实“逆天”,但通过 ts-morph 的节点寻址 是可以实现的。
策略:使用 // @generated 作为“防线”
在 Elysia 中,我们不再把注释写在整个变量上,而是写在调用链的方法前。
代码生成器的视角:
TypeScript
export const userController = new Elysia({ prefix: "/user" })
.use(dbPlugin)
// @generated
.get("/", () => userService.list()) // 这里的 .get 紧跟在注释后,它是安全的替换目标
.get("/me", () => { ... }) // 这个方法没有注释,生成器会直接跳过它,即使它在链条中间
// @generated
.post("/", () => userService.create())
实现原理:
-
解析链条:
ts-morph可以通过getExpression()一层层剥开CallExpression。 -
查找注释:利用
getLeadingCommentRanges()检查当前调用节点(如.get)上方是否有// @generated。 -
局部重组:如果检测到标记,通过
replaceWithText仅替换该段链式代码。
3. 完整的自动化流程设计 (CLI 层)
第一步:autogen init (环境初始化)
-
生成
autogen.config.ts。 -
在
script/templates下生成原子化的方法模板(例如service/list.ts.txt)。
第二步:autogen scan (元数据同步)
-
扫描 Schema:读取
table.schema.ts里的@gen注释。 -
智能识别:
-
如果有
@gen all,在manifest中为该实体开启所有层级。 -
如果是中间表且没有注释,自动标记为
skip。
-
-
写入 Manifest:更新
autogen.config.ts里的manifest对象。
第三步:autogen sync (多后端精准喷涂)
-
多路径循环:读取
outputs数组,依次访问每个项目目录。 -
原子化注入:
-
Service: 检查类方法上的
/** @generated */。 -
Controller: 检查 Elysia 链条中的
// @generated。 -
Frontend Hook: 检查导出函数上的
// @generated。
-
-
自动注册:
-
更新
index.ts。 -
更新
app-router.ts:寻找appRouter变量,插入.use(xxxController)。
-
4. 为什么这个结构对 AI 最友好?
-
明确的边界:AI 只要看到
// @generated,就知道这一块是它“可以接管”的领地;看到没有标记的代码,它会自动绕行。 -
模板的可预测性:由于模板被抽离到了
templates/文件夹,AI 可以非常容易地通过微调模板来改变全系统的生成风格,而不需要修改核心逻辑。 -
配置的紧凑性:
manifest对象充当了 AI 的“任务清单”,它不需要扫描几千行代码,只需要看清单就知道哪些表需要生成哪些方法。
5. 建议的开发路线图
-
重构
ast-utils.ts:增加针对CallExpression链条的注释检查和替换逻辑。 -
实现
TemplateEngine:支持从外部文件夹加载方法体字符串,并替换__TABLE__等占位符。