@pylonts/dsl 1.1.20 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregate.d.ts +3 -13
- package/dist/aggregate.js +19 -22
- package/dist/dto.d.ts +18 -2
- package/dist/dto.js +75 -9
- package/dist/flow-script.js +73 -7
- package/dist/flow.d.ts +20 -3
- package/dist/flow.js +82 -11
- package/dist/mermaid-driver.js +6 -1
- package/dist/repository.d.ts +2 -2
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +12 -4
- package/docs/aggregate-implementation.md +174 -0
- package/docs/aggregate.md +49 -12
- package/docs/dto.md +130 -106
- package/docs/table.md +41 -0
- package/docs/utils.md +19 -12
- package/package.json +1 -1
- package/src/aggregate.ts +32 -41
- package/src/dto.ts +87 -8
- package/src/flow-script.ts +68 -6
- package/src/flow.ts +99 -17
- package/src/mermaid-driver.ts +5 -1
- package/src/repository.ts +2 -2
- package/src/utils.ts +18 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# 聚合 DSL 极简落盘记录
|
|
2
|
+
|
|
3
|
+
> 日期:本次会话
|
|
4
|
+
> 范围:`@pylonts/dsl` 聚合声明层
|
|
5
|
+
> 目标:让“多对一 / 一对多”先落地,扩展表(一对一)只设计不实现。
|
|
6
|
+
|
|
7
|
+
## 1. 背景
|
|
8
|
+
|
|
9
|
+
聚合判断标准已收敛为:
|
|
10
|
+
|
|
11
|
+
> 创建 / 保存 / 删除时,是否必须一起做?
|
|
12
|
+
> - 是 → 聚合成员
|
|
13
|
+
> - 否 → 不聚合(引用)
|
|
14
|
+
|
|
15
|
+
本次落盘只实现最常用的“一对多”聚合成员:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
defineAggregate({
|
|
19
|
+
root: order,
|
|
20
|
+
members: {
|
|
21
|
+
items: [orderItem],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- 数组成员 `[orderItem]` = 一对多;
|
|
27
|
+
- 非数组成员(扩展表 / 一对一)暂不落代码,保留设计。
|
|
28
|
+
|
|
29
|
+
## 2. 本次落盘改了什么
|
|
30
|
+
|
|
31
|
+
### 2.1 源码
|
|
32
|
+
|
|
33
|
+
| 文件 | 改动 |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `dsl/src/aggregate.ts` | 极简化 `defineAggregate` |
|
|
36
|
+
| `dsl/src/repository.ts` | 注释同步为 `FK / extends` |
|
|
37
|
+
|
|
38
|
+
`dsl/src/aggregate.ts` 关键变化:
|
|
39
|
+
|
|
40
|
+
- 去掉 `name` 必填,聚合名自动取 `root.name`;
|
|
41
|
+
- `members` 类型简化为:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
Record<string, TableSchema | TableSchema[]>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- 删除 `via` / `one` 复杂配置;
|
|
48
|
+
- 数组成员校验:
|
|
49
|
+
- 必须恰好包含一张表;
|
|
50
|
+
- 该表必须有且仅有一个外键指向 root 主键;
|
|
51
|
+
- 多个外键指向 root 时明确报错;
|
|
52
|
+
- 非数组成员(扩展表)暂不支持,明确报错。
|
|
53
|
+
|
|
54
|
+
### 2.2 测试
|
|
55
|
+
|
|
56
|
+
| 文件 | 改动 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `dsl/test/aggregate.test.ts` | 改为新极简 API,并增加“非数组成员暂不支持”测试 |
|
|
59
|
+
| `dsl/test/repository.test.ts` | 改为新极简 API |
|
|
60
|
+
|
|
61
|
+
### 2.3 文档
|
|
62
|
+
|
|
63
|
+
| 文件 | 改动 |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `dsl/docs/aggregate.md` | 示例去掉 `name`,成员表达改为数组/扩展表,同步扩展表规则 |
|
|
66
|
+
| `dsl/docs/table.md` | 新增“扩展表(extends)”设计说明,标注暂不落代码 |
|
|
67
|
+
| `dsl/docs/aggregate-implementation.md` | 本文档 |
|
|
68
|
+
|
|
69
|
+
## 3. 当前 API 形态
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { defineAggregate } from '@pylonts/dsl';
|
|
73
|
+
|
|
74
|
+
const orderAggregate = defineAggregate({
|
|
75
|
+
root: order,
|
|
76
|
+
members: {
|
|
77
|
+
items: [orderItem],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
约束:
|
|
83
|
+
|
|
84
|
+
- `root` 必须有主键;
|
|
85
|
+
- `members` 的数组元素必须是普通表;
|
|
86
|
+
- 数组元素必须有外键指向 `root` 的主键;
|
|
87
|
+
- 如果有多条外键指向 root,必须收敛为一条,否则无法自动推导。
|
|
88
|
+
|
|
89
|
+
## 4. 如何验证
|
|
90
|
+
|
|
91
|
+
在仓库根目录或 `dsl/` 目录执行。
|
|
92
|
+
|
|
93
|
+
### 4.1 运行 DSL 测试
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# 方式一:workspace
|
|
97
|
+
npm test --workspace @pylonts/dsl
|
|
98
|
+
|
|
99
|
+
# 方式二:进入 dsl 目录
|
|
100
|
+
cd dsl
|
|
101
|
+
npm test
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
可只跑聚合相关测试:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd dsl
|
|
108
|
+
npx vitest run test/aggregate.test.ts test/repository.test.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
预期结果:
|
|
112
|
+
|
|
113
|
+
- `aggregate.test.ts` 全部通过;
|
|
114
|
+
- `repository.test.ts` 全部通过;
|
|
115
|
+
- 新增的“非数组成员暂不支持”用例通过。
|
|
116
|
+
|
|
117
|
+
### 4.2 类型检查
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run typecheck --workspace @pylonts/dsl
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
或:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cd dsl
|
|
127
|
+
npm run typecheck
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
预期结果:无 TypeScript 错误。
|
|
131
|
+
|
|
132
|
+
### 4.3 构建
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm run build --workspace @pylonts/dsl
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
或:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd dsl
|
|
142
|
+
npm run build
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
预期结果:`dist/` 正常生成。
|
|
146
|
+
|
|
147
|
+
### 4.4 全量回归(如项目要求)
|
|
148
|
+
|
|
149
|
+
在仓库根按项目现有脚本执行:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm run lint all
|
|
153
|
+
npm run typecheck
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
或按仓库实际脚本执行 `api tsc + lint all`。
|
|
157
|
+
|
|
158
|
+
## 5. 验证重点
|
|
159
|
+
|
|
160
|
+
1. 新的极简 `defineAggregate` 能正常声明聚合;
|
|
161
|
+
2. 不再需要传 `name`;
|
|
162
|
+
3. `members.items` 直接是 `[orderItem]`,不再有 `via` / `one`;
|
|
163
|
+
4. 没有外键指向 root 时会报错;
|
|
164
|
+
5. 非数组成员会报“扩展表未实现”;
|
|
165
|
+
6. 旧测试已全部迁移到新 API。
|
|
166
|
+
|
|
167
|
+
## 6. 暂未实现(保留设计)
|
|
168
|
+
|
|
169
|
+
- 扩展表 `extends: root`:一对一成员;
|
|
170
|
+
- 非数组成员:当前会抛错;
|
|
171
|
+
- Repository 生成器:仍未实现;
|
|
172
|
+
- flow 调用 repository:仍未实现。
|
|
173
|
+
|
|
174
|
+
这些内容在 `dsl/docs/aggregate.md` 和 `dsl/docs/table.md` 中保留设计。
|
package/docs/aggregate.md
CHANGED
|
@@ -24,17 +24,54 @@
|
|
|
24
24
|
| 应用服务编排 | service_schema + flow ✅ | 已具备 |
|
|
25
25
|
| 防腐层 | ThirdServiceSchema 只隔离 | 缺模型映射 |
|
|
26
26
|
|
|
27
|
-
## 2.
|
|
27
|
+
## 2. 什么情况下需要聚合?
|
|
28
|
+
|
|
29
|
+
**核心判断标准**:
|
|
30
|
+
|
|
31
|
+
> 创建 / 保存 / 删除时,是否必须一起做?
|
|
32
|
+
> - 是 → 聚合(成员)
|
|
33
|
+
> - 否 → 不聚合(引用)
|
|
34
|
+
|
|
35
|
+
### 2.1 成员 vs 引用
|
|
36
|
+
|
|
37
|
+
| 维度 | 聚合成员 | 聚合间引用 |
|
|
38
|
+
|------|---------|-----------|
|
|
39
|
+
| 生命周期 | 与根同生共死 | 独立生命周期 |
|
|
40
|
+
| 创建 | 随根一起创建 | 不随根创建 |
|
|
41
|
+
| 保存 | 随根一起保存 | 不随根保存 |
|
|
42
|
+
| 删除 | 随根级联删除 | 不随根删除 |
|
|
43
|
+
| 关联 | 成员表 FK 指向根 / 扩展表 extends 根 | 本表 FK 指向其他聚合根 |
|
|
44
|
+
| 示例 | order_item / order_address 快照 | order.user_id / order.address_id |
|
|
45
|
+
|
|
46
|
+
### 2.2 案例:地址可复用 vs 地址快照
|
|
47
|
+
|
|
48
|
+
**场景**:一个订单对应一个地址,但地址可以被多个订单复用。
|
|
49
|
+
|
|
50
|
+
- 如果地址可复用 → 不是聚合成员,是引用:
|
|
51
|
+
- 创建订单不会创建地址;
|
|
52
|
+
- 删除订单不会删除地址;
|
|
53
|
+
- 多个订单可以指向同一个地址;
|
|
54
|
+
- 建模为 `references: { address: 'AddressAggregate' }`,外键不需要 unique。
|
|
55
|
+
|
|
56
|
+
- 如果地址是下单时的快照(copy)→ 是聚合成员:
|
|
57
|
+
- 下单时复制一份地址到订单;
|
|
58
|
+
- 之后修改地址簿不影响已下单订单;
|
|
59
|
+
- 删除订单时该快照一起删除;
|
|
60
|
+
- 建模为 `members: { address: orderAddress }`;
|
|
61
|
+
- 此时 `orderAddress` 应为**扩展表**(`extends: order`),主键与根主键同义、类型一致,天然保证一对一。
|
|
62
|
+
|
|
63
|
+
**lint 规则**:声明为一对一成员时,成员表必须是扩展表(`extends` 指向根),主键与根主键同义且类型一致;否则它要么是一对多,要么应改为 references。
|
|
64
|
+
|
|
65
|
+
## 3. 扩展一:AggregateSchema(核心)
|
|
28
66
|
|
|
29
67
|
**作用**:显式声明"哪些表属于同一个聚合、谁是聚合根、成员如何挂载、跨成员不变式、聚合间引用规则"。这是把"多表一致性从约定变约束"的落点。
|
|
30
68
|
|
|
31
69
|
```ts
|
|
32
70
|
defineAggregate({
|
|
33
|
-
name: 'Order',
|
|
34
71
|
root: ordersTable, // 聚合根表
|
|
35
|
-
members: { //
|
|
36
|
-
items:
|
|
37
|
-
address:
|
|
72
|
+
members: { // 成员表:数组=一对多,非数组=一对一
|
|
73
|
+
items: [orderItemsTable], // 1:N,外键自动推导
|
|
74
|
+
address: orderAddressTable, // 1:1,扩展表
|
|
38
75
|
},
|
|
39
76
|
invariants: [ // 跨成员表不变式,挂聚合上,可被生成代码消费
|
|
40
77
|
{ name: 'total = sum(items.price * items.qty)',
|
|
@@ -47,8 +84,8 @@ defineAggregate({
|
|
|
47
84
|
| 声明项 | 含义 | 缺了会怎样 |
|
|
48
85
|
|--------|------|-----------|
|
|
49
86
|
| `root` | 谁是聚合根 | 分不清一致性入口 |
|
|
50
|
-
| `members` | 包含哪几个 table schema |
|
|
51
|
-
|
|
|
87
|
+
| `members` | 包含哪几个 table schema;数组=普通表=1:N,非数组=扩展表=1:1 | 无法推导成员关系 |
|
|
88
|
+
| 成员外键 | 由 TableSchema.foreignKeys / extends 自动推导 | 工具无法推导 join / 级联关系 |
|
|
52
89
|
| `invariants` | 跨成员一致性规则 | "总额=Σ明细"又回到 flow 里手工写 |
|
|
53
90
|
| `references` | 聚合间只按 ID 引用 | 无法 lint 跨聚合直接持表引用 |
|
|
54
91
|
|
|
@@ -60,7 +97,7 @@ defineAggregate({
|
|
|
60
97
|
|
|
61
98
|
**多表映射三种模式**(聚合↔表):A 单表=单聚合(1:1,几乎透明);B 一聚合=多表(1:N,最常见,Order 案例);C 多聚合共享表(N:1,DDD 不推荐)。
|
|
62
99
|
|
|
63
|
-
##
|
|
100
|
+
## 4. 扩展二:RepositorySchema(可推导,也可显式声明)
|
|
64
101
|
|
|
65
102
|
**作用**:聚合粒度的存储入口,把"哪些表一起查、怎么拼成聚合"的知识从 service flow 下沉到仓储。调用方只面对领域概念(Order),不面对表。
|
|
66
103
|
|
|
@@ -76,7 +113,7 @@ defineRepository({
|
|
|
76
113
|
|
|
77
114
|
**聚合内 join 下沉、聚合间禁止 join**:
|
|
78
115
|
|
|
79
|
-
- 聚合内:加载整个 Order(根 + 明细 + 地址)由 Repository 内部完成,调用方一行,join
|
|
116
|
+
- 聚合内:加载整个 Order(根 + 明细 + 地址)由 Repository 内部完成,调用方一行,join 知识由成员表 `foreignKeys` / `extends` 自动推导;
|
|
80
117
|
- 聚合间:只按 ID 引用,跨聚合 join 是 DDD 禁止的;展示商品名这类信息走读模型 / 查询服务(CQRS 的 Q 侧),或应用服务分步查 + 内存组装。
|
|
81
118
|
|
|
82
119
|
**分层对照**:
|
|
@@ -87,7 +124,7 @@ defineRepository({
|
|
|
87
124
|
| Repository | **聚合** | orders + order_items 一个事务 | **本扩展** |
|
|
88
125
|
| DAO | **表** | 单表一条 SQL | dao_schema ✅ |
|
|
89
126
|
|
|
90
|
-
##
|
|
127
|
+
## 5. 扩展三:引入支持 DDD 的 TS 库(选型,待决策)
|
|
91
128
|
|
|
92
129
|
dsl 是**声明期**(`defineAggregate` 声明结构),引入的库是**运行期**(代码跑起来时持久化/发事件)。两者互补,不冲突——声明可翻译为运行期库的配置。
|
|
93
130
|
|
|
@@ -101,9 +138,9 @@ dsl 是**声明期**(`defineAggregate` 声明结构),引入的库是**运
|
|
|
101
138
|
|
|
102
139
|
**建议**:运行时持久化参考/选用 **MikroORM**(聚合持久化设计最完整);领域事件参考 **Remesh**。dsl 的 `AggregateSchema` 声明层本身无现成开源,属本项目的增量设计空间。
|
|
103
140
|
|
|
104
|
-
##
|
|
141
|
+
## 6. 落地步骤(待办,未开工)
|
|
105
142
|
|
|
106
|
-
1. `AggregateSchema` 类型 + `defineAggregate` + 定义期校验(root
|
|
143
|
+
1. `AggregateSchema` 类型 + `defineAggregate` + 定义期校验(root 必须在其表内、成员表必须有外键指向 root 或 extends root、成员表不能是其他聚合的 root 等);
|
|
107
144
|
2. `RepositorySchema` 类型 + `defineRepository`(或从 aggregate 自动推导生成);
|
|
108
145
|
3. `gen` 生成 Repository 代码:加载/保存/删除三套骨架 + 同事务包装(`@Trans()` 从声明推导);
|
|
109
146
|
4. `lint` 聚合边界检查:聚合外禁止直接改成员表、聚合间禁止跨表引用;
|
package/docs/dto.md
CHANGED
|
@@ -1,107 +1,131 @@
|
|
|
1
|
-
# 定义 DTO(四种方向)
|
|
2
|
-
|
|
3
|
-
DTO 描述接口出入参。方向决定可选性规则,由各方向工厂设置;**规则统一只在 `field.optional === undefined` 时生效**(作者已显式设置的跳过):
|
|
4
|
-
|
|
5
|
-
| 构建器 | 方向 | 可选性规则 |
|
|
6
|
-
|---|---|---|
|
|
7
|
-
| `buildInput` | input | 按 DB 列规则:主键 → 必填;可空(未写 `optional` 或 `optional: true`)/ 有默认 → 可选;`optional: false` 无默认 → 必填 |
|
|
8
|
-
| `buildOutput` | output | 不设置(保持字段构建器/作者设置) |
|
|
9
|
-
| `buildQuery` | query | 全部可选 |
|
|
10
|
-
| `buildPk` | pk | 主键字段必填,其他字段可选 |
|
|
11
|
-
|
|
12
|
-
## 从字段集合投影
|
|
13
|
-
|
|
14
|
-
`from(source, fields)` 接受两类字段集合源:**表**(`TableSchema`)或**第三方方法消息**(`ThirdMethodSchema`,见 [third-service.md](./third-service.md)),投影字段包装为 DTO 字段,字段实例与源共享,`name/schema` 保持指向源。
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';
|
|
18
|
-
|
|
19
|
-
// 输入:新增订单
|
|
20
|
-
buildInput('OrderAddRequest', { ...from(order, [order.columns.mer_id, order.columns.amount]) });
|
|
21
|
-
|
|
22
|
-
// 输出:订单行
|
|
23
|
-
buildOutput('OrderRow', from(order, [order.columns.id, order.columns.order_no]));
|
|
24
|
-
|
|
25
|
-
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
26
|
-
buildQuery('OrderPageQuery', {
|
|
27
|
-
keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
|
|
28
|
-
...from(order, [order.columns.mer_id]),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// 主键:按 id 取详情
|
|
32
|
-
buildPk('OrderDetailRequest', from(order, [order.columns.id]));
|
|
33
|
-
|
|
34
|
-
// 转发:透传第三方方法消息(wire-format 字段名保持协议原样,不转 camelCase)
|
|
35
|
-
buildOutput('BalanceResult', from(queryBalance.results, [queryBalance.results.fields.balance]));
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
- **表源**:DTO 字段名转 camelCase(`mer_id` → `merId`),与 DB 列名(snake_case)分离。
|
|
39
|
-
- **第三方方法源**:字段名即线格式协议名(`out_trade_no`、`appId`),保持不变。
|
|
40
|
-
- `from()` 本身不做任何可选性推断——推断在各方向工厂,且按字段的 schema 判断:共享实体列按列规则推断(Rule A),wire 自有字段保留声明值。
|
|
41
|
-
|
|
42
|
-
## 独立字段
|
|
43
|
-
|
|
44
|
-
不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`、`default`。
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
dtoField(stringField({ maxLength: 32 })).setOperator('like')
|
|
48
|
-
dtoField(intField()).setDefault(0) // TypeBox default 注解
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
# 定义 DTO(四种方向)
|
|
2
|
+
|
|
3
|
+
DTO 描述接口出入参。方向决定可选性规则,由各方向工厂设置;**规则统一只在 `field.optional === undefined` 时生效**(作者已显式设置的跳过):
|
|
4
|
+
|
|
5
|
+
| 构建器 | 方向 | 可选性规则 |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `buildInput` | input | 按 DB 列规则:主键 → 必填;可空(未写 `optional` 或 `optional: true`)/ 有默认 → 可选;`optional: false` 无默认 → 必填 |
|
|
8
|
+
| `buildOutput` | output | 不设置(保持字段构建器/作者设置) |
|
|
9
|
+
| `buildQuery` | query | 全部可选 |
|
|
10
|
+
| `buildPk` | pk | 主键字段必填,其他字段可选 |
|
|
11
|
+
|
|
12
|
+
## 从字段集合投影
|
|
13
|
+
|
|
14
|
+
`from(source, fields)` 接受两类字段集合源:**表**(`TableSchema`)或**第三方方法消息**(`ThirdMethodSchema`,见 [third-service.md](./third-service.md)),投影字段包装为 DTO 字段,字段实例与源共享,`name/schema` 保持指向源。
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';
|
|
18
|
+
|
|
19
|
+
// 输入:新增订单
|
|
20
|
+
buildInput('OrderAddRequest', { ...from(order, [order.columns.mer_id, order.columns.amount]) });
|
|
21
|
+
|
|
22
|
+
// 输出:订单行
|
|
23
|
+
buildOutput('OrderRow', from(order, [order.columns.id, order.columns.order_no]));
|
|
24
|
+
|
|
25
|
+
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
26
|
+
buildQuery('OrderPageQuery', {
|
|
27
|
+
keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
|
|
28
|
+
...from(order, [order.columns.mer_id]),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 主键:按 id 取详情
|
|
32
|
+
buildPk('OrderDetailRequest', from(order, [order.columns.id]));
|
|
33
|
+
|
|
34
|
+
// 转发:透传第三方方法消息(wire-format 字段名保持协议原样,不转 camelCase)
|
|
35
|
+
buildOutput('BalanceResult', from(queryBalance.results, [queryBalance.results.fields.balance]));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **表源**:DTO 字段名转 camelCase(`mer_id` → `merId`),与 DB 列名(snake_case)分离。
|
|
39
|
+
- **第三方方法源**:字段名即线格式协议名(`out_trade_no`、`appId`),保持不变。
|
|
40
|
+
- `from()` 本身不做任何可选性推断——推断在各方向工厂,且按字段的 schema 判断:共享实体列按列规则推断(Rule A),wire 自有字段保留声明值。
|
|
41
|
+
|
|
42
|
+
## 独立字段
|
|
43
|
+
|
|
44
|
+
不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`、`default`。
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
dtoField(stringField({ maxLength: 32 })).setOperator('like')
|
|
48
|
+
dtoField(intField()).setDefault(0) // TypeBox default 注解
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 容器字段:禁止内联嵌套(构建期校验)
|
|
52
|
+
|
|
53
|
+
**DTO 不内联**——嵌套结构必须命名引用,构建期(`buildInput/buildOutput/buildQuery/buildPk` 及 `defineUtils` args)机器校验:
|
|
54
|
+
|
|
55
|
+
| 形态 | 允许 | 禁止 |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| 数组字段 `dtoArrayField` | `items: 命名 DTO`(`items: orderItemDto` → `OrderItemDto[]`)或 `items: 标量字段`(`string[]`) | `items: dtoObjectField(...)` 内联对象元素、`items: dtoArrayField(...)` 内联数组元素 |
|
|
58
|
+
| 对象字段 | 裸 `objectField(...)`(wire 格式嵌套,渲染内联 `Type.Object({...})`) | `dtoObjectField(...)`、`dtoField(dtoObjectField(...))`——DtoField 类容器必须命名引用(数组元素),或改用裸 `objectField` |
|
|
59
|
+
|
|
60
|
+
**理由**:DtoField 类容器(`dtoObjectField` / `dtoArrayField`)内联时无法跨 DTO 复用,生成类型要么靠字符串拼接(`Array<{...}>`),要么塌缩(`any[]`)——命名引用让结构有单一事实来源(DTO 定义处)且生成精确类型。裸 `objectField` / `arrayField` 属于 wire 格式嵌套(随消息直接内联渲染),不在禁用范围。校验错误示例:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
[dto] "OrderSubmitRequest" field "items": inline container elements are not allowed — array items must be a named DTO or a scalar field
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// ✗ 内联对象元素
|
|
68
|
+
items: dtoArrayField({ items: dtoObjectField({ properties: { skuId: ..., qty: ... } }) })
|
|
69
|
+
|
|
70
|
+
// ✓ 命名 DTO 引用(OrderItemDto 单独 buildOutput 声明)
|
|
71
|
+
export const OrderItemDto = buildOutput('OrderItemDto', { skuId: ..., qty: ... });
|
|
72
|
+
items: dtoArrayField({ items: OrderItemDto })
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 字段引用规格(Reference Spec)
|
|
76
|
+
|
|
77
|
+
`DtoField` 通过 `setRef(other)` 或共享字段实例引用其他字段,表达"本字段来源于 X"。**引用不是任意的**——只有业务契约 DTO(controller/service 消费的 `dto_schema/{app}` 或 `dto_schema/common` DTO)的字段可以作为引用方,且只能引用三类被引用方:
|
|
78
|
+
|
|
79
|
+
| # | 引用 | 语法 | 语义 |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| 1 | **field 引用** | `dtoField(table.columns.x)`(共享实例)或 `setRef` 指向包装表列的 DtoField | 与数据库字段同义(来源:库) |
|
|
82
|
+
| 2 | **token 引用** | `fromToken(token, [...])` 或 `setRef(token 字段)` | 字段来源于登录身份(服务端注入) |
|
|
83
|
+
| 3 | **third 引用** | `setRef(third 消息字段)` | 字段来源于第三方消息(参数或结果) |
|
|
84
|
+
|
|
85
|
+
**方向约束**:
|
|
86
|
+
|
|
87
|
+
- 引用方:业务契约 DTO 的字段;
|
|
88
|
+
- 被引用方:token 字段 / third 消息字段 / 表列 field——**业务 DTO 之间不能互相引用**(convert 的 sources/target 各自独立,映射由搬运生成推导,不靠 DTO 互 ref);
|
|
89
|
+
- third 字段、token 字段不可反向引用业务 DTO;
|
|
90
|
+
- ref 链无环(渲染期强校验,循环引用报错)。
|
|
91
|
+
|
|
92
|
+
**对 convert 自动搬运的意义**:同源判定 = ref 链终端 / 共享 field 实例,且只有规格内合法引用构成可推导搬运——业务 DTO ← third(防腐翻译,`setRef(thirdField)` = "本字段从 third 参数/结果来")、业务 DTO ← entity(共享表列实例)都是可自动生成搬运的映射;越界引用是声明错误。
|
|
93
|
+
|
|
94
|
+
> entity 列是裸 `Field` 实例:DTO 字段 `dtoField(order.columns.x)` 与 entity 列共享同一实例即构成 field 引用。
|
|
95
|
+
|
|
96
|
+
### token 引用的注入与 readOnly
|
|
97
|
+
|
|
98
|
+
`fromToken(token, fields)` 是 token 引用的标准入口:每个投影字段 `setRef(token 字段)`(复用类型/约束)+ 标记 `injectFrom`(服务端注入)。typebox 生成物中注入字段渲染为 **`Type.Optional(...)` + `readOnly: true` 注解**(JSON Schema annotation)——服务器字段,客户端不得上送:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
id: Type.Optional(Type.Integer({ readOnly: true })),
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
同时生成非枚举 `__inject` 适配器,fastify RPC 层在进 controller 前执行 `body.k = token.k` 从登录身份填充——**客户端即使上送也会被覆盖**(上送无效)。三层闭环:声明 readOnly(不可上送语义)+ Optional(可不传)+ 运行时覆盖(上送无效)。
|
|
105
|
+
|
|
106
|
+
注意:**只有 `fromToken()` 自动设置 `injectFrom`**;手写 `setRef(token 字段)` 只表达引用关系,不触发注入渲染(如需注入须显式标记或改走 fromToken)。
|
|
107
|
+
|
|
108
|
+
## 默认值
|
|
109
|
+
|
|
110
|
+
- `setDefault(v)` 设 DTO 层默认值,渲染为 TypeBox `default:` 注解(`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`)。
|
|
111
|
+
- 枚举默认值必须是该枚举的成员值(value),渲染时解析为成员引用;非成员值直接报错。
|
|
112
|
+
- 未显式设置时,fallback 到字段构建器的 `default`(DB 默认值,string),`from()` 提取的字段自动带出。
|
|
113
|
+
|
|
114
|
+
## 继承基础 schema
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
buildQuery('OrderPageQuery', { ... })
|
|
118
|
+
.include({ from: '@pylonts/core', name: 'PageRequest' }); // 渲染 Type.Intersect([PageRequest, ...])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 关键语义
|
|
122
|
+
|
|
123
|
+
- **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
|
|
124
|
+
- **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
|
|
125
|
+
- **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
|
|
126
|
+
|
|
127
|
+
## 文件组织
|
|
128
|
+
|
|
129
|
+
- `dto_schema/{module}/*.dto.ts`:DTO 源文件,一个文件可导出多个 DtoMessage;`module` = `project.config.ts` 的 `apps.name`,另加 `common/` 放跨 app 共享 DTO。
|
|
130
|
+
- `pylonts gen dto` 按 module 逐个扫描:`dto_schema/{module}/*.dto.ts` → 生成 `dto/{module}/{Name}.dto.ts`。
|
|
107
131
|
- 枚举字段引用 `enums/` 下生成的枚举源文件,DTO 文件通过 `../../enums/{JsName}.enum` 导入。
|
package/docs/table.md
CHANGED
|
@@ -131,6 +131,47 @@ export const audit = defineTable('audit', {
|
|
|
131
131
|
|
|
132
132
|
> **外键是逻辑作用**:`foreignKeys` 用于定义期命名强校验与关系表达,**DDL 默认不渲染物理 FOREIGN KEY 约束**(`pylonts gen sql init` 不传 `generateForeignKeys`)。数据完整性由 Service/DAO 层保证;如需物理约束,调用 `buildCreateTableSql(schema, { generateForeignKeys: true })`。
|
|
133
133
|
|
|
134
|
+
## 扩展表(extends)
|
|
135
|
+
|
|
136
|
+
扩展表表示“本表是某张根表的延伸”:主键与根表主键同义、类型一致,生命周期跟随根表(创建 / 保存 / 删除一起做)。除这两条外,扩展表与普通表完全一样,可以有索引、外键、被其他表引用等。
|
|
137
|
+
|
|
138
|
+
### 定义
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// order 根表
|
|
142
|
+
export const order = defineTable('order', {
|
|
143
|
+
...
|
|
144
|
+
primaryKey: id,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// order_address 是 order 的扩展表
|
|
148
|
+
const addressId = stringField({ maxLength: 32 }); // 与 order.id 同类型
|
|
149
|
+
|
|
150
|
+
export const orderAddress = defineTable('order_address', {
|
|
151
|
+
extends: order, // 声明本表是 order 的扩展
|
|
152
|
+
primaryKey: addressId, // 主键与根主键同义
|
|
153
|
+
columns: {
|
|
154
|
+
id: addressId,
|
|
155
|
+
receiver_name: stringField({ label: '收货人', maxLength: 32, optional: false }),
|
|
156
|
+
...
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 规则
|
|
162
|
+
|
|
163
|
+
- 扩展表的主键与根表主键同义,类型必须一致;
|
|
164
|
+
- 根表不能是扩展表;
|
|
165
|
+
- 扩展表不能再 `extends`(禁止链式延伸);
|
|
166
|
+
- 一个根表可以有多个扩展表;
|
|
167
|
+
- 除主键和生命周期外,扩展表与普通表完全一样:可以有普通外键、索引、枚举等,也可被其他表引用;
|
|
168
|
+
- 扩展表不需要像普通外键那样命名 `{phrase}_{field}`,也不需要显式声明 `foreignKeys` 来表达与根的关系,`extends` 本身就是关系。
|
|
169
|
+
|
|
170
|
+
### 与聚合的关系
|
|
171
|
+
|
|
172
|
+
- `members: { items: [orderItem] }`:数组 → 普通表 → 一对多;
|
|
173
|
+
- `members: { address: orderAddress }`:非数组 → 扩展表 → 一对一。
|
|
174
|
+
|
|
134
175
|
## 生成 SQL
|
|
135
176
|
|
|
136
177
|
见 [driver.md](./driver.md)。
|
package/docs/utils.md
CHANGED
|
@@ -46,7 +46,8 @@ export default {
|
|
|
46
46
|
|
|
47
47
|
- **args**:`Record<string, DtoField>`——用 `dtoField(...)` 包装。可包装**表列**(`dtoField(order.columns.status)`,领域规则引用表字段的标准方式)或内联字段(`dtoField(stringField(...))`)。包装层反写不污染共享表列实例(与 `buildMessage` 同规则:仅内联字段写回底层)。
|
|
48
48
|
- **result**:`Field | undefined`——输出是全新值(boolean 判断、decimal 计算、string 派生文案),**不是共享表列**;省略 = void 方法(防御 guard 通过时无返回值)。
|
|
49
|
-
-
|
|
49
|
+
- **throws**:`ExceptionSchema[] | undefined`——方法可抛的异常清单,**防御 guard 的失败契约**。flow 里 `invoke` 防御 guard 时,其 throws 自动进入流程逃逸集——调用方 service 方法的 throws 必须包含它(或 TRY 捕获)。谓词(can/is/has)不抛,无需声明。
|
|
50
|
+
- **失败语义**:规则失败直接 `throw`(配合 `throws` 声明的 `BusinessException`/`CodeException` 通道),**不返回错误码结构**。
|
|
50
51
|
|
|
51
52
|
## 文件与存储约定
|
|
52
53
|
|
|
@@ -65,11 +66,11 @@ export default {
|
|
|
65
66
|
|
|
66
67
|
从纯函数视角,方法的出口只有两种:**返回值**或**抛异常**。因此领域规则方法恰好三类:
|
|
67
68
|
|
|
68
|
-
| 分类 | 命名 | 本质 | result | 失败语义 | 典型示例 |
|
|
69
|
-
|
|
70
|
-
| **1 防御 guard** | `assertXX` / `validateXX` / `ensureXX` | 条件抛错(前置校验) | `void` | **失败必抛错**,通过无返回值 | `assertCancelable`——已完成/已取消订单 throw |
|
|
71
|
-
| **2 判断** | `canXX` / `isXX` / `hasXX` | 返回判定值(谓词) | `boolean`
|
|
72
|
-
| **3 计算** | `calcXX` / `formatXX` / 动词 | 派生值计算 | 标量(金额/数量/文案) | 一般不抛(入参合法前提) | `calcTotal`——金额计算 |
|
|
69
|
+
| 分类 | 命名 | 本质 | result | throws | 失败语义 | 典型示例 |
|
|
70
|
+
|---|---|---|---|---|---|---|
|
|
71
|
+
| **1 防御 guard** | `assertXX` / `validateXX` / `ensureXX` | 条件抛错(前置校验) | `void` | **必填**(失败契约) | **失败必抛错**,通过无返回值 | `assertCancelable`——已完成/已取消订单 throw `BusinessException` |
|
|
72
|
+
| **2 判断** | `canXX` / `isXX` / `hasXX` | 返回判定值(谓词) | `boolean` | 无 | 返回 false,不抛 | `canCancel`、`isExpired`——供 flow `IF(...)` 条件位 |
|
|
73
|
+
| **3 计算** | `calcXX` / `formatXX` / 动词 | 派生值计算 | 标量(金额/数量/文案) | 一般不声明 | 一般不抛(入参合法前提) | `calcTotal`——金额计算 |
|
|
73
74
|
|
|
74
75
|
### 为什么这就是全集
|
|
75
76
|
|
|
@@ -77,12 +78,18 @@ export default {
|
|
|
77
78
|
- **防御 guard = 判断 + 抛错**:guard 内部必然先判断再 throw,是"判断"失败分支的显式化;通过时没有调用方需要的值,所以返回 `void`。
|
|
78
79
|
- **void 方法只能是防御 guard**:纯函数无副作用,不返回也不抛 = 什么都没做——void 方法若默认通过、特殊状态才抛,就是 guard 的语义。
|
|
79
80
|
|
|
80
|
-
###
|
|
81
|
+
### 命名与返回的一致性(机器校验,已落地)
|
|
81
82
|
|
|
82
|
-
- `assert/validate/ensure` 前缀 → **必须 void**(guard
|
|
83
|
-
- `can/is/has` 前缀 → **必须 boolean
|
|
83
|
+
- `assert/validate/ensure` 前缀 → **必须 void + 必须声明 throws**(guard:通过无值、失败抛契约异常);
|
|
84
|
+
- `can/is/has` 前缀 → **必须 boolean**(判断谓词,进 flow `IF` 条件位);
|
|
84
85
|
- 计算类 → 标量 result。
|
|
85
|
-
|
|
86
|
+
|
|
87
|
+
**双重校验**:
|
|
88
|
+
|
|
89
|
+
1. **声明侧**(`pylonts lint utils`):命名与 result 形态不匹配(如 `canXX` 返回 void、`assertXX` 返回 boolean)即违规报错;防御 guard 缺 throws 声明(失败契约缺失)同样违规。
|
|
90
|
+
2. **使用侧**(flow 编译期,`defineFlow` 校验):条件位谓词必须声明 boolean result——`IF(invoke(xxUtils.canCancel, ...))` 通过;`IF(invoke(xxUtils.assertCancelable, ...))`(void 守卫)和标量方法进 IF 直接报错,提示"守卫应 invoke 而非 IF"。
|
|
91
|
+
|
|
92
|
+
**逃逸集联动**:`invoke` 防御 guard 时,guard 的 throws 自动并入流程逃逸集(与 dao/third 的 throws 同规则)——调用方 service 方法契约因此必须声明该异常(或 TRY 捕获),守卫的失败成为可验证的契约,而不是隐式冒泡。
|
|
86
93
|
|
|
87
94
|
## 建议(best practices)
|
|
88
95
|
|
|
@@ -93,7 +100,7 @@ export default {
|
|
|
93
100
|
5. **纯函数、无 I/O**:utils 不做数据库/网络访问;需要 I/O 的规则属于 service。
|
|
94
101
|
6. **表列引用用 `dtoField(table.columns.x)` 包装**:包装层反写安全,共享列实例永不被动;参数名可以与列名不同(如 `state` 包装 `status` 列)。
|
|
95
102
|
7. **result 选型与命名一致**:防御 guard → **`void`** + `assert/validate/ensure` 前缀(抛错即语义);判断 → `booleanField()`(或枚举)+ `can/is/has` 前缀(供 flow 谓词位);计算 → `decimalField`/`intField`/`stringField` 标量。
|
|
96
|
-
8. **flow 集成**:判断类可直接作 `IF(cond)` 条件位谓词(多入参靠 invoke
|
|
103
|
+
8. **flow 集成**:判断类可直接作 `IF(cond)` 条件位谓词(多入参靠 invoke 多槽位),`gen service --flow` 渲染为 `OrderUtils.canCancel(body.status)`;防御 guard 通过 `invoke` 在流程前置位调用,渲染为 `OrderUtils.assertCancelable(body.status);`(void、内部 throw)。**计算类绑定标量槽**:flow 声明 `slots: { total: decimalField(...) }`,`invoke(calcTotal, args, slots.total)` 渲染 `const total: string = OrderUtils.calcTotal(body.unitPrice);`,标量槽可直接进守卫比较(`IF(gt(slots.total, 100))` → `if (Number(total) > 100)`)或作后续调用的标量参数(jsType 匹配直传)。
|
|
97
104
|
|
|
98
105
|
## 生成与合并
|
|
99
106
|
|
|
@@ -101,4 +108,4 @@ export default {
|
|
|
101
108
|
|
|
102
109
|
## 校验
|
|
103
110
|
|
|
104
|
-
`pylonts lint utils`:存储位置 + 绑定共享实例 +
|
|
111
|
+
`pylonts lint utils`:存储位置 + 绑定共享实例 + 文件名 + **命名↔result 一致性**(assert/validate/ensure → void,can/is/has → boolean)机器校验(`lint all` 已包含)。使用侧谓词 boolean 校验在 flow 编译期(`defineFlow`)执行。
|