生成产物与重新生成
生成的是 TypeScript 源码,不是独立 HTTP 实现。装饰器类需启用 experimentalDecorators: true 编译;安装实际输出 import 的包。
产物类型
| 产物 | 生成规则 |
|---|---|
types.ts | 按 schema 名称空间分组的组件模型类型/枚举及导入的 Wow 类型 |
*ApiClient.ts | 带 operationId 的普通 tagged 操作;操作全部 tag 都必须是可用 API tag;排除 wow/Actuator/Wow 聚合 tag |
commandClient.ts | 已解析聚合的命令路径、请求体别名、普通及流式命令客户端 |
queryClient.ts | 聚合 QueryClientFactory、状态/字段类型、领域事件联合(空集为 never)及事件标题枚举 |
boundedContext.ts | 已解析上下文的别名常量 |
index.ts | .ts 文件及非空子目录的递归导出 |
.wow-generator.json | 版本 1 所有权清单,记录生成 .ts 文件的 SHA-256;旧的 .fetcher-generator.json 会被读取一次并替换 |
源码约定
每个生成文件都以 // Code generated by wow-generator. DO NOT EDIT. 开头,使用单引号、两空格缩进和分号。相对导入以 .js 结尾,每个 barrel 以 ./dir/index.js 重新导出,因此输出在 "moduleResolution": "NodeNext" 和 "bundler" 下都能编译。
导入在生成时显式写出,所以无论输出目录能否解析 @ahoo-wang/*,输出都逐字节相同。保存前生成器会检查每个文件都声明或导入了它用到的名字、且没有重复声明;不满足时不写入任何文件。这项检查不能替代消费方的编译器。
两个同级模块都导出的名字(例如两个包里同名的模型)不会进入它们共同的 index.ts,并给出警告;请从它自己的模块导入。
模型的文档注释默认只含摘要:标题、描述、schema 键、format、默认值、示例和约束。--schema-docs full(选项 schemaDocs: 'full')还会嵌入完整的 JSON schema。
方法
方法名依次取:配置中的 apiClients[tag].methodNames[operationId]、操作的 x-fetcher-method 扩展,否则取 operationId 按点号分隔的最后一段并转为 camelCase:
| operationId | 方法名 |
|---|---|
getUserById | getUserById |
delete_user_by_id | deleteUserById |
getUser_1 | getUser1 |
users.list | list |
example.cart.add_cart_item | addCartItem |
方法名只取决于操作本身,所以新增操作不会让已有方法改名。同一客户端的两个操作得到相同方法名时,本次运行以退出码 4 失败,并点名这两个操作;用 methodNames 或 x-fetcher-method 给其中一个命名即可。
普通方法把每个 path、query、header 参数都生成为带类型的位置参数,以参数名命名(item-id → itemId),请求体则是单独的 @body() 参数。必填的在前,按文档顺序排列——path、query、header,然后是请求体——可选的在后,调用方不必为了传必填参数而先传 undefined。最后是 httpRequest?: ParameterRequest(用于请求可设置的其他内容:额外请求头、超时、signal)和 attributes?: Record<string, unknown>:
search(@path('item-id') itemId: string, @query('q') q: string,
@header('X-Tenant') xTenant: string, @query('page') page?: number,
@query('status') status?: 'open' | 'closed',
@request() httpRequest?: ParameterRequest,
@attribute() attributes?: Record<string, unknown>): Promise<Item[]>| 参数或请求体 | 生成的类型 |
|---|---|
| path 参数 | 始终必填;ignorePathParameters 会省略其中一部分 |
| query 或 header 参数 | 文档写 required: true 时必填;原始类型枚举生成为其字面量的联合 |
| cookie 参数 | 省略并给出警告:cookie 由浏览器发送,fetch 无法设置 |
JSON 请求体(application/json、+json) | schema 的类型;schema 未要求的属性通过 PartialBy<Item, 'id'> 保持可选 |
multipart/form-data | FormData |
application/x-www-form-urlencoded | URLSearchParams |
text/* | string |
| 其他媒体类型 | BodyInit |
requestBody.required 为 true 时请求体必填。参数的描述会成为方法文档注释中的 @param 标签。路径级参数会继承,操作级参数按 in/name 覆盖。缺少 operationId 或 tag 的操作会被跳过并给出警告。
返回类型取自成功响应:200,否则取最小的其他 2xx,再否则取 2XX。任何 JSON 媒体类型(application/json、+json,带不带 charset 均可)按其 schema 推导,然后是 wildcard schema,再是 SSE。text/* 以及 wildcard 下的字符串 schema 返回 Promise<string>;识别 SSE 后返回 JsonServerSentEventStream,无法推导事件模型时回退到 any。无法推导正文时回退为 Promise<Response>,提取原生 Response。生成方法依赖装饰器在运行时替换 throw autoGeneratedError(...) 占位实现。
名字与 schema
- 名字会转为标识符:参数
item-id→itemId,schemaPage«User»→PageUser,1stThing→_1stThing,命令pay-order→PAY_ORDER与payOrder。描述中的*/会被转义。 - 两个 schema 归一化后生成同一个模型时以退出码 4 失败。归一化后相同的枚举值会得到不同的成员。归一化后指向同一客户端的 tag 会生成带编号的类(
User2ApiClient),并给出警告。 - 指向不存在目标的
$ref以退出码 4 失败,并列出这些引用。 { nullable: true, allOf: [{ $ref }] }允许null。带 discriminator 的oneOf按判别属性收窄各分支。以自身为值的 map 生成带索引签名的接口。名为Record或Response的模型以别名导入,不会遮蔽全局类型。- 空的命令或事件体为
Record<string, never>。
Wow 查询 schema 映射为 @ahoo-wang/wow-client 的类型。带 filter 属性的 wow.api.query.ListQuery、PagedQuery schema(Wow 8.11 及以后)映射为根入口的 FilterListQuery、FilterPagedQuery;wow.api.query.Condition、ConditionOptions、Operator,以及不带 filter 的 ListQuery、PagedQuery schema(Wow 8.10)映射为 @ahoo-wang/wow-client/legacy 中已弃用的类型,该子路径在 v10 删除。
运行时配置
pnpm add @ahoo-wang/fetcher @ahoo-wang/fetcher-decorator @ahoo-wang/fetcher-eventstream @ahoo-wang/wow-client
pnpm exec wow-generator generate -i ./openapi.json -o ./src/generated -t ./tsconfig.json
pnpm exec tsc --noEmit -p ./tsconfig.json通过 ApiMetadata 构造器创建生成的客户端,通常传 { fetcher }。为目标服务配置 Fetcher.baseURL;生成过程不调用生成 API。
命令客户端,以及文档带 x-wow-context-alias 时的 API 客户端,会把构造器收到的 apiMetadata 合并到默认值之上,所以 new CartCommandClient({ fetcher }) 保留限界上下文的基础路径,请求发往 /example/...。流式命令客户端继承同一个构造器。不经按上下文别名路由的网关、直接访问服务时,传 basePath: '';查询客户端工厂对应传 contextAlias: '':
import { Fetcher } from '@ahoo-wang/fetcher';
import { CartCommandClient, cartQueryClientFactory } from './generated/index.js';
const fetcher = new Fetcher({ baseURL: 'http://localhost:8080' });
const commands = new CartCommandClient({ fetcher, basePath: '' });
const snapshots = cartQueryClientFactory.createSnapshotQueryClient({
fetcher,
contextAlias: '',
});查询客户端工厂的 aggregateName 是从聚合快照路由读出的路由段,例如以 @AggregateRoute(resourceName = "sales-order") 声明的聚合 order 为 sales-order。字段类型参数是 `${OrderAggregatedFields}`,即字段枚举的字符串值:枚举成员和对应字符串都能编译,拼错的字段不能。把查询标注为 ListQuery 或 FilterListQuery(字段默认为 string)的代码需要写明字段类型:ListQuery<`${CartAggregatedFields}`>。参见声明式端点。
所有权与失败
再次生成到相同路径的文件会被替换:手写定制应放在生成文件之外。陈旧文件仅在旧清单记录且内容 hash 未改变时删除。已修改的陈旧文件及无关文件会保留;保留不代表它属于当前生成 API。重建 index 仍可能包含项目中已有的源文件。
清单无效或生成路径逃出输出根目录会抛错。保存完成后才删除陈旧文件并写新清单,但整个目录不是原子事务:失败可能留下部分写入。不要通过删清单强制清理;使用专用输出目录并审查再生成差异。生成器自身的名字检查不能证明输出在你的依赖下能通过类型检查,必须执行消费者编译器。
实现源码
typescript/wow-generator/src/utils/sourceFiles.ts
typescript/wow-generator/src/client/apiClientGenerator.ts