@pylonts/dsl 1.1.20 → 1.1.22
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/README.md +8 -5
- package/dist/action.d.ts +76 -33
- package/dist/action.js +19 -17
- package/dist/aggregate.d.ts +3 -13
- package/dist/aggregate.js +19 -22
- package/dist/component.d.ts +3 -3
- package/dist/curd.d.ts +2 -2
- 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/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/journey.d.ts +23 -0
- package/dist/journey.js +26 -0
- package/dist/mermaid-driver.js +6 -1
- package/dist/navigation.d.ts +2 -2
- package/dist/page-action.d.ts +39 -0
- package/dist/page-action.js +17 -0
- package/dist/page-def.d.ts +9 -9
- package/dist/page-flow.d.ts +4 -4
- package/dist/popup.d.ts +3 -3
- package/dist/repository.d.ts +2 -2
- package/dist/task.d.ts +20 -0
- package/dist/task.js +21 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +12 -4
- package/docs/aggregate-implementation.md +174 -0
- package/docs/aggregate.md +147 -110
- package/docs/concepts.md +109 -0
- package/docs/dto.md +130 -106
- package/docs/table.md +41 -0
- package/docs/task.md +81 -0
- package/docs/utils.md +19 -12
- package/package.json +1 -1
- package/src/action.ts +87 -52
- package/src/aggregate.ts +94 -103
- package/src/component.ts +3 -3
- package/src/curd.ts +2 -2
- package/src/dto.ts +87 -8
- package/src/flow-script.ts +68 -6
- package/src/flow.ts +99 -17
- package/src/index.ts +3 -0
- package/src/journey.ts +61 -0
- package/src/mermaid-driver.ts +5 -1
- package/src/navigation.ts +2 -2
- package/src/page-action.ts +59 -0
- package/src/page-def.ts +9 -9
- package/src/page-flow.ts +4 -4
- package/src/popup.ts +3 -3
- package/src/repository.ts +35 -35
- package/src/task.ts +52 -0
- package/src/utils.ts +18 -4
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/task.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 任务 (Task)
|
|
2
|
+
|
|
3
|
+
Task 是**定时任务(定时器)**的定义——**action 闭包的第二成员**(action = controller | task | third callback,无第四种)。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
action = trigger =
|
|
7
|
+
① controller —— 前端/外部调用的 RPC 入口
|
|
8
|
+
② task —— 定时器(cron 驱动):到期退款、结算、提现、报表、关单
|
|
9
|
+
③ third callback —— 外部系统主动回调
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- **Task 只表示定时器**(cron 驱动)。异步/事件驱动的操作归 event 体系(EventObserver / EventNotifier,待设计),不属于 task。
|
|
13
|
+
- Task 是**契约**(名字 + cron + 做什么),不声明状态变化——状态变化由 journey 步骤表达。
|
|
14
|
+
- Task 实现(扫描逻辑、幂等)在实现层(如 pylon-flow step)。
|
|
15
|
+
|
|
16
|
+
## 定义
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// task_schema/auto-refund.task.ts
|
|
20
|
+
export const AutoRefundTask = defineTask({
|
|
21
|
+
name: 'AutoRefundTask',
|
|
22
|
+
label: '到期自动退款',
|
|
23
|
+
cron: '0 3 * * *',
|
|
24
|
+
description: '扫描已锁定券码 + auto_refund + valid_to<now → 按订单发起全额退款',
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 字段
|
|
29
|
+
|
|
30
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `name` | string | ✅ | `XxTask`(PascalCase + `Task` 后缀) |
|
|
33
|
+
| `label` | string | ✅ | 中文名 |
|
|
34
|
+
| `cron` | string | ✅ | 定时表达式(系统怎么触发的契约,一处看全) |
|
|
35
|
+
| `description` | string | 可选 | 做什么(叙述) |
|
|
36
|
+
|
|
37
|
+
## 命名与存储规则
|
|
38
|
+
|
|
39
|
+
| 约定 | 规则 | 例子 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `name` | PascalCase + `Task` 后缀 | `AutoRefundTask` |
|
|
42
|
+
| 导出符号 | = name | `export const AutoRefundTask` |
|
|
43
|
+
| 文件位置 | `task_schema/`(项目根,与 schema/ 并列) | `task_schema/auto-refund.task.ts` |
|
|
44
|
+
| 文件名 | name 去 `Task` 后缀转 kebab + `.task.ts` | `auto-refund.task.ts` |
|
|
45
|
+
| 一文件一 task | loader 机器校验(仿 loadDaos/loadEntities) | 多导出/零导出报错 |
|
|
46
|
+
|
|
47
|
+
## 校验
|
|
48
|
+
|
|
49
|
+
- 运行时(`defineTask`):
|
|
50
|
+
- `name` 必须以 `Task` 结尾,否则抛错
|
|
51
|
+
- `cron` 必填(task 是定时器),否则抛错
|
|
52
|
+
- 存储(`loadTasks`):
|
|
53
|
+
- 唯一合法目录是 `task_schema/` 根(一级)
|
|
54
|
+
- **导出符号 == schema name**,否则抛错
|
|
55
|
+
- 文件名 = name 去 `Task` 后缀转 kebab + `.task.ts`,否则抛错
|
|
56
|
+
- 一文件一 task(多导出/零导出报错)
|
|
57
|
+
|
|
58
|
+
## 作为 action 引用
|
|
59
|
+
|
|
60
|
+
Task 与 controller、third callback 并列,是 action 闭包成员,被状态迁移与 journey 步骤引用:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// journey 步骤
|
|
64
|
+
{ action: AutoRefundTask, host: api, text: '到期自动退款' }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
lint 校验:`action` 引用必须是三类之一(controller | task | third callback)且引用存在。
|
|
68
|
+
|
|
69
|
+
## 与 pylon-flow 的关系
|
|
70
|
+
|
|
71
|
+
pylon-flow 的 step 函数是 task 的实现形态之一:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// flow/flows/settlement.flow.ts(api driver)
|
|
75
|
+
export async function autoRefund(flow, deps) { ... } // 实现 AutoRefundTask
|
|
76
|
+
export default { autoRefund };
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **task schema = 契约**(名字/cron/做什么)
|
|
80
|
+
- **pylon-flow step = 实现**(怎么跑)
|
|
81
|
+
- 对账:task schema 的 name ↔ pylon-flow 的 step names——"定时任务已声明但没实现"可 lint
|
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`)执行。
|
package/package.json
CHANGED
package/src/action.ts
CHANGED
|
@@ -1,52 +1,87 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { ControllerMethodSchema } from './controller.js';
|
|
3
|
+
import type { ThirdServiceMethodSchema, ThirdCallbackSchema } from './third-service.js';
|
|
4
|
+
import type { TaskSchema } from './task.js';
|
|
5
|
+
import type { TableSchema } from './db.js';
|
|
6
|
+
import type { PageSchema } from './page.js';
|
|
7
|
+
|
|
8
|
+
// Cross-domain actions: what happens along a business line (journey).
|
|
9
|
+
// Distinguished from PageActionSchema (what a user can do ON a page):
|
|
10
|
+
// an Action is one beat of a journey — a page visit, an API call, a
|
|
11
|
+
// third-party invocation, a database write, or a task trigger.
|
|
12
|
+
//
|
|
13
|
+
// Every action is `type + properties + data`: the type picks the property
|
|
14
|
+
// set (page for page visits, method for controller/third, table for db,
|
|
15
|
+
// task for task triggers), and `data` is the input of that beat. No further
|
|
16
|
+
// classification — page/controller/third/db/task are all actions, only their
|
|
17
|
+
// properties differ.
|
|
18
|
+
|
|
19
|
+
/** A beat of a journey: type + properties + input data. */
|
|
20
|
+
export interface ActionSchema extends SchemaBase {
|
|
21
|
+
type: 'page' | 'controller' | 'third' | 'db' | 'task';
|
|
22
|
+
/** Input data of this beat (blueprint: names first, refined to refs later). */
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Visit a page — `data` present means fill/submit a form, absent means pure view. */
|
|
27
|
+
export interface PageAction extends ActionSchema {
|
|
28
|
+
type: 'page';
|
|
29
|
+
/** Target page (PageFlow node). */
|
|
30
|
+
page: PageSchema;
|
|
31
|
+
/** Page url/path (e.g. '/bd/apply'). */
|
|
32
|
+
url: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Call a backend controller method. */
|
|
36
|
+
export interface ControllerAction extends ActionSchema {
|
|
37
|
+
type: 'controller';
|
|
38
|
+
/** The controller method invoked (shared instance). */
|
|
39
|
+
method: ControllerMethodSchema;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Invoke a third-party service method or receive its callback. */
|
|
43
|
+
export interface ThirdAction extends ActionSchema {
|
|
44
|
+
type: 'third';
|
|
45
|
+
/** The third-party method invoked (outbound). */
|
|
46
|
+
method?: ThirdServiceMethodSchema;
|
|
47
|
+
/** The third-party callback received (inbound) — mutually exclusive with method. */
|
|
48
|
+
callback?: ThirdCallbackSchema;
|
|
49
|
+
/** Wait for the async callback (inbound) before the journey continues. */
|
|
50
|
+
async?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Write to a database table. */
|
|
54
|
+
export interface DbAction extends ActionSchema {
|
|
55
|
+
type: 'db';
|
|
56
|
+
/** The table written (shared instance). */
|
|
57
|
+
table: TableSchema;
|
|
58
|
+
/** Write operation: insert | update | delete. */
|
|
59
|
+
op: 'insert' | 'update' | 'delete';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Trigger a system task (scheduled or async). */
|
|
63
|
+
export interface TaskAction extends ActionSchema {
|
|
64
|
+
type: 'task';
|
|
65
|
+
/** The task triggered (shared instance). */
|
|
66
|
+
task: TaskSchema;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Builder for a journey action. */
|
|
70
|
+
export const action = {
|
|
71
|
+
page(options: { page: PageSchema; url: string; data?: Record<string, unknown>; description?: string }): PageAction {
|
|
72
|
+
return { name: options.page.name, type: 'page', page: options.page, url: options.url, data: options.data, description: options.description };
|
|
73
|
+
},
|
|
74
|
+
controller(options: { method: ControllerMethodSchema; data?: Record<string, unknown>; description?: string }): ControllerAction {
|
|
75
|
+
return { name: options.method.name, type: 'controller', method: options.method, data: options.data, description: options.description };
|
|
76
|
+
},
|
|
77
|
+
third(options: { method?: ThirdServiceMethodSchema; callback?: ThirdCallbackSchema; data?: Record<string, unknown>; async?: boolean; description?: string }): ThirdAction {
|
|
78
|
+
const name = options.method?.name ?? options.callback?.name ?? 'third';
|
|
79
|
+
return { name, type: 'third', method: options.method, callback: options.callback, data: options.data, async: options.async, description: options.description };
|
|
80
|
+
},
|
|
81
|
+
db(options: { table: TableSchema; op: 'insert' | 'update' | 'delete'; data?: Record<string, unknown>; description?: string }): DbAction {
|
|
82
|
+
return { name: options.table.name, type: 'db', table: options.table, op: options.op, data: options.data, description: options.description };
|
|
83
|
+
},
|
|
84
|
+
task(options: { task: TaskSchema; data?: Record<string, unknown>; description?: string }): TaskAction {
|
|
85
|
+
return { name: options.task.name, type: 'task', task: options.task, data: options.data, description: options.description };
|
|
86
|
+
},
|
|
87
|
+
};
|