@pylonts/dsl 1.1.6 → 1.1.12

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.
Files changed (88) hide show
  1. package/README.md +4 -0
  2. package/dist/action.d.ts +32 -0
  3. package/dist/action.js +14 -0
  4. package/dist/aggregate.d.ts +38 -0
  5. package/dist/aggregate.js +46 -0
  6. package/dist/business-flow.d.ts +9 -0
  7. package/dist/business-flow.js +72 -0
  8. package/dist/controller.d.ts +17 -9
  9. package/dist/controller.js +8 -2
  10. package/dist/convert.d.ts +28 -10
  11. package/dist/convert.js +16 -5
  12. package/dist/curd.d.ts +7 -10
  13. package/dist/curd.js +3 -1
  14. package/dist/dao.d.ts +81 -53
  15. package/dist/dao.js +291 -12
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +10 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +17 -2
  21. package/dist/dsl.js +7 -0
  22. package/dist/dto.d.ts +6 -4
  23. package/dist/dto.js +5 -4
  24. package/dist/entity.d.ts +29 -0
  25. package/dist/entity.js +13 -0
  26. package/dist/exception.d.ts +9 -3
  27. package/dist/exception.js +25 -1
  28. package/dist/expr.d.ts +45 -0
  29. package/dist/expr.js +32 -0
  30. package/dist/filter.d.ts +45 -0
  31. package/dist/filter.js +21 -0
  32. package/dist/flow-script.d.ts +108 -0
  33. package/dist/flow-script.js +505 -0
  34. package/dist/flow.d.ts +294 -17
  35. package/dist/flow.js +803 -18
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.js +6 -2
  38. package/dist/mermaid-driver.js +264 -24
  39. package/dist/mysql-driver.js +3 -0
  40. package/dist/project.d.ts +10 -6
  41. package/dist/project.js +35 -4
  42. package/dist/repository.d.ts +26 -0
  43. package/dist/repository.js +8 -0
  44. package/dist/service.d.ts +14 -2
  45. package/dist/service.js +49 -0
  46. package/dist/third-service.d.ts +5 -0
  47. package/dist/third-service.js +1 -0
  48. package/dist/typebox-driver.js +4 -0
  49. package/dist/utils.d.ts +9 -2
  50. package/dist/utils.js +4 -0
  51. package/docs/aggregate.md +110 -0
  52. package/docs/curd.md +146 -111
  53. package/docs/dao-generation.md +478 -0
  54. package/docs/ddd-principles.md +75 -0
  55. package/docs/domain-event.md +137 -0
  56. package/docs/keyword-matcher.md +182 -0
  57. package/docs/project.md +17 -9
  58. package/docs/token.md +327 -0
  59. package/docs/trans-reentrant.md +85 -0
  60. package/package.json +25 -6
  61. package/src/action.ts +51 -10
  62. package/src/aggregate.ts +104 -0
  63. package/src/business-flow.ts +80 -0
  64. package/src/controller.ts +25 -11
  65. package/src/convert.ts +51 -15
  66. package/src/curd.ts +12 -6
  67. package/src/dao.ts +377 -63
  68. package/src/db.ts +13 -0
  69. package/src/domain-event.ts +74 -0
  70. package/src/dsl.ts +23 -2
  71. package/src/dto.ts +9 -6
  72. package/src/entity.ts +43 -0
  73. package/src/exception.ts +30 -5
  74. package/src/expr.ts +65 -0
  75. package/src/filter.ts +70 -0
  76. package/src/flow-script.ts +696 -0
  77. package/src/flow.ts +1129 -46
  78. package/src/index.ts +6 -2
  79. package/src/mermaid-driver.ts +256 -29
  80. package/src/mysql-driver.ts +3 -0
  81. package/src/project.ts +138 -97
  82. package/src/repository.ts +35 -0
  83. package/src/service.ts +68 -3
  84. package/src/third-service.ts +6 -0
  85. package/src/typebox-driver.ts +4 -0
  86. package/src/utils.ts +13 -2
  87. package/src/endpoint.ts +0 -18
  88. package/src/provider.ts +0 -68
@@ -0,0 +1,137 @@
1
+ # 领域事件 DSL 扩展规划(Domain Event)
2
+
3
+ > 状态:**部分落地(步骤 1 声明层 ✅ / 步骤 3 @Trans afterCommit ✅ / 步骤 4 outbox DDL ✅ / 发布 API ✅,其余待实现)**
4
+ > 关联代码:`dsl/src/event.ts`(UI 事件,非领域事件)、`dsl/src/flow.ts`(flow IR)、`pylon-dao/src/trans.ts`(`@Trans()`)
5
+ > 背景对话:ts-libs 会话「dd DDD 扩展讨论」;姊妹篇:[aggregate.md](./aggregate.md)(聚合/仓储规划)
6
+
7
+ ## 1. 背景:pylon 事件基础设施现状
8
+
9
+ - 整个 pylon 体系目前**零事件基础设施**:flow.ts 无 event,仓库无 publish/subscribe/EventEmitter;
10
+ - `dsl/src/event.ts` 是 **UI 组件事件**(Tab onChange 的 `e.detail`),与领域事件完全无关;
11
+ - flow 只有 `invoke`(调用方法)/ `write`(写槽)/ `THROW` / `RETURN`,**没有"发布事件"动作,也没有事件声明**。
12
+
13
+ **命令 vs 事件**(必须分清):
14
+
15
+ | | Command(命令) | Event(事件) |
16
+ |---|---|---|
17
+ | 语义 | "去做某事"(意图) | "某事已经发生"(事实) |
18
+ | 时态 | 未来/祈使 | 过去/陈述 |
19
+ | 命名 | `PlaceOrder`、`CancelOrder` | `OrderPlaced`、`OrderCancelled` |
20
+ | 接收方 | 有且一个(命令处理器) | 零到多个订阅者 |
21
+ | 失败处理 | 调用方要感知失败 | 发布方不关心谁处理 |
22
+ | dsl 对应 | flow 的 `invoke(m)` | **新概念** |
23
+
24
+ **事件驱动的解耦价值**:命令式让 flow 依赖所有下游(扣库存、发短信、加积分都要在 flow 里 invoke);事件式让 flow 只依赖"事实"(事件名 + 数据),下游变化不影响 flow。
25
+
26
+ ## 2. 事件生命周期(五步)
27
+
28
+ ```
29
+ ① 发生:Order.cancel() 状态 CREATED → CANCELLED
30
+ ② 收集:聚合根在内存记录"我取消了"
31
+ ③ 发布:事务提交成功后投递 OrderCancelled
32
+ ④ 订阅:库存上下文、通知服务各自注册监听
33
+ ⑤ 处理:各订阅者做自己的事(扣库存、发短信)
34
+ ```
35
+
36
+ 关键在 ②→③ 的**发布时机**,决定一致性。
37
+
38
+ ## 3. 发布时机:三种策略
39
+
40
+ | 策略 | 做法 | 问题 |
41
+ |------|------|------|
42
+ | A. 事务内发 | tx { 改状态; 发事件; } | 订阅者失败回滚主事务;跨服务时事务管不到对方 |
43
+ | B. 事务后发 | tx { 改状态; } → 发事件 | 发的时候进程崩了 → 事件丢失,状态改了没人知道 |
44
+ | **C. Transactional Outbox(生产标准)** | tx { 改状态; INSERT outbox; } → 后台 relay 投递 | **保证:状态和事件要么都成要么都败,事件最终必达** |
45
+
46
+ **pylon 现状对照**:knex 透明代理 + `@Trans()` 让"更新状态 + 写 outbox 表"同事务天然可行(都是 DAO 调用),outbox 在 pylon 技术上现成,缺的是声明层。
47
+
48
+ ## 4. dsl 扩展方向:三件新东西
49
+
50
+ ```ts
51
+ // ① 事件声明(一等 schema,携带数据快照)
52
+ defineDomainEvent({
53
+ name: 'OrderCancelled',
54
+ fields: { orderNo: str, reason: str }, // 快照,不是引用
55
+ });
56
+
57
+ // ② flow 里加 publish action(编译成同事务写 outbox 表)
58
+ flowScript('cancelOrder', { args, slots }, ({ next, slots }) => {
59
+ next(
60
+ invoke(orderService.load, { id: slots.orderId }, 'order'),
61
+ invoke(order.cancel), // 状态流转
62
+ publish('OrderCancelled', { orderNo: slots.order.orderNo, reason: 'user' }), // 新动作
63
+ );
64
+ return flowEnd.ok;
65
+ });
66
+
67
+ // ③ 订阅声明(复用 flow 编排处理逻辑)
68
+ defineEventHandler({
69
+ name: 'onOrderCancelled',
70
+ event: 'OrderCancelled',
71
+ flow: handleOrderCancelledFlow, // 处理流程复用现有 flow 模型
72
+ });
73
+ ```
74
+
75
+ - 发布是**声明不是副作用**:`publish(...)` 在 flow 里显式可见,审查时一眼看到"此流程发什么事件";
76
+ - 不用引入消息队列:outbox 表 + 后台 relay 即够(BLE 案例规模不需要 RabbitMQ);
77
+ - 订阅:启动时扫描 handler 注册到总线 / 消费组。
78
+
79
+ ## 5. 发布技术选型(结论)
80
+
81
+ **发布侧:已定死 —— MySQL outbox 表**(与业务同事务,knex 现成):
82
+
83
+ ```sql
84
+ CREATE TABLE event_outbox (
85
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
86
+ event_name VARCHAR(100) NOT NULL, -- 'OrderCancelled'
87
+ payload JSON NOT NULL, -- 事件数据快照
88
+ status ENUM('pending','delivered') DEFAULT 'pending',
89
+ created_at DATETIME DEFAULT NOW()
90
+ );
91
+ ```
92
+
93
+ **投递侧:按部署形态分档**:
94
+
95
+ | 方案 | 传输介质 | 延迟 | 依赖 | 适用 |
96
+ |------|---------|------|------|------|
97
+ | **A. 进程内总线** | Node 内存(EventEmitter) | 0 | 无 | **单进程(pylon 默认)** |
98
+ | **B. MySQL 轮询 relay** | 扫 outbox 表 | ~轮询间隔 | 无(现成) | 单进程但要持久化兜底 |
99
+ | **C. Redis Stream** | Redis | 毫秒 | ioredis(sign-redis-driver 已有) | 多进程/多服务 |
100
+ | **D. 消息队列** | RabbitMQ / Kafka | 毫秒 | 重型中间件 | 生产大规模,超出 pylon 定位 |
101
+
102
+ **推荐路径**:
103
+
104
+ - **单进程(pylon 默认,先做)**:A + B 结合,零新依赖——事务提交后(afterCommit)进程内直接分发(延迟 0),outbox 兜底补投失败/崩溃遗留的 pending 记录(不丢事件)。`@Trans()` 加 afterCommit 钩子即可:
105
+
106
+ ```ts
107
+ // trans.ts 扩展:事务提交成功后执行注册的回调
108
+ export function Trans() {
109
+ return function (target, key, descriptor) {
110
+ const original = descriptor.value;
111
+ descriptor.value = async function (...args) {
112
+ return knex.transaction(async (trx) => {
113
+ const afterCommits: Array<() => Promise<void>> = [];
114
+ return txStorage.run(trx, async () => {
115
+ const result = await original.apply(this, args);
116
+ await trx.executionPromise; // 等事务真正提交
117
+ await Promise.all(afterCommits.map(fn => fn()));
118
+ return result;
119
+ });
120
+ });
121
+ };
122
+ };
123
+ }
124
+ ```
125
+
126
+ - **多进程/多服务(未来升级)**:方案 C Redis Stream——pylon 已有 ioredis(sign-redis-driver),Redis 不算新基建;outbox relay 从进程内分发换成 `XADD`,订阅服务 `XREADGROUP` 消费。
127
+ - **方案 D 不做**:超出 pylon"薄封装、快速开发"定位。
128
+
129
+ **订阅侧**:`defineEventHandler` 声明 + 启动扫描注册;复用 flow 编排处理逻辑,不需要新执行模型。
130
+
131
+ ## 6. 落地步骤(待办,未开工)
132
+
133
+ 1. `DomainEventSchema` 类型 + `defineDomainEvent` + 定义期校验(字段类型、事件名唯一)✅;
134
+ 2. flow 加 `publish` action:编译成"同事务插 outbox 表"的 DAO 调用 + afterCommit 回调注册;
135
+ 3. `@Trans()` 扩展 afterCommit 钩子(`pylon-dao/src/trans.ts`)✅;4. `event_outbox` 表:DDL 由 gen 生成(`defineDomainEvent` 自动建表)✅(独立包 `@pylonts/event` 已含发布 API:`publishEvent` + `@EventNotifier`,outbox DDL 由 `gen-outbox.ts` + `pylonts gen sql init` 追加);
136
+ 5. relay:单进程(进程内分发 + 启动/定时补投)→ 跨进程(Redis Stream);
137
+ 6. `defineEventHandler` + 启动扫描注册订阅者。
@@ -0,0 +1,182 @@
1
+ # Keyword 智能匹配扩展规划(Keyword Matcher)
2
+
3
+ > 状态:**规划中(未实现)**
4
+ > 关联代码:`dsl/src/filter.ts`(FilterSchema.keyword)、`gen/src/filter-render.ts`(keyword OR-like 渲染)、`curd/src/service.ts`(keyword query 端点)
5
+ > 背景:curd 搜索框输一个值,需要判断它"像什么"(手机号 / 订单号 / 商户号),再决定精确查询哪一列;不像任何已知形状才回退模糊匹配。
6
+
7
+ ## 1. 背景:问题与现状
8
+
9
+ **问题**:管理页搜索框只有一个 keyword 输入,业务上常见输入是"可直接定位到一行的值":
10
+
11
+ | 用户输入 | 业务含义 | 期望查询 |
12
+ |---------|---------|---------|
13
+ | `13800138000` | 手机号(11 位,1 开头) | `user.phone = '13800138000'`(精确) |
14
+ | `ORD17230000001234` | 订单号(ORD 前缀) | `order.order_no = ...`(精确) |
15
+ | `M17230001234` | 商户号(M 前缀) | `merchant.mer_no = ...`(精确) |
16
+ | `张三` | 人名 | 名称列 LIKE(模糊) |
17
+
18
+ **现状**:`FilterSchema.keyword = { columns: Field[] }` 只有一种行为——对声明列 OR 拼接 `LIKE %kw%`:
19
+
20
+ ```ts
21
+ // 现状声明(filter_schema/examples-api/admin/filter/user-list.filter.ts)
22
+ keyword: { columns: [user.columns.username] },
23
+ ```
24
+
25
+ ```ts
26
+ // 现状产物(api/src/modules/admin/filter/UserListFilter.ts)
27
+ if (args.keyword) {
28
+ q.where(function (this: QueryBuilder) {
29
+ this.where('user.username', 'like', `%${args.keyword}%`);
30
+ });
31
+ }
32
+ ```
33
+
34
+ 手机号按 LIKE 查 `username` 永远查不到(username 是昵称),用户输入"138..."期望按手机号定位用户却得到空结果——**输入形状与查询列不匹配**。
35
+
36
+ **形状知识现状**:`@pylonts/mock` 的生成器已经持有形状约定(生成侧),识别侧没有:
37
+
38
+ | 生成器 | 形状 | 位置 |
39
+ |--------|------|------|
40
+ | `mockPhone` | 11 位,运营商前缀段(134/135/... 白名单) | `pylon-mock/src/generators/phone.ts` |
41
+ | `mockMerchantId` | `M` + 8 位时间戳 + 2 位随机 | `pylon-mock/src/generators/id.ts` |
42
+ | `mockOrderId` | `ORD` + 时间戳 + 2 位随机 | `pylon-mock/src/generators/id.ts` |
43
+ | 身份证 / 银行卡 | 生成规则已存在 | `pylon-mock/src/generators/identity.ts`、`data/bankcard.json` |
44
+
45
+ 生成与识别是同一形状的两面,识别器应从同一形状知识推导,而不是在 filter 声明里再写一遍。
46
+
47
+ ## 2. 方案:keyword.rules 形状规则
48
+
49
+ `FilterSchema.keyword` 扩展 `rules`,声明"keyword 像什么时精确查哪一列":
50
+
51
+ ```ts
52
+ keyword: {
53
+ columns: [user.columns.username], // fallback:仍按模糊 OR-like
54
+ rules: [
55
+ { field: user.columns.phone, when: 'phone' }, // 内置语义名
56
+ { field: order.columns.order_no, when: /^ORD\d+$/ }, // 项目自定义正则
57
+ ],
58
+ },
59
+ ```
60
+
61
+ ```ts
62
+ // dsl/src/filter.ts 类型扩展
63
+ export interface KeywordRule {
64
+ /** 命中后精确查询的列。 */
65
+ field: Field;
66
+ /** 形状判定:内置语义名(字符串)或项目正则(RegExp 字面量)。 */
67
+ when: string | RegExp;
68
+ }
69
+
70
+ export interface FilterSchema {
71
+ // ...
72
+ keyword?: {
73
+ columns: Field[];
74
+ /** 形状规则,按声明顺序判定,首个命中短路(可选)。 */
75
+ rules?: KeywordRule[];
76
+ };
77
+ }
78
+ ```
79
+
80
+ **判定语义**:
81
+
82
+ 1. `keyword` 为空 → 整个 keyword 块跳过(现状不变);
83
+ 2. 遍历 `rules`(**声明顺序即判定顺序**,确定性):
84
+ - `when` 是字符串 → 查内置语义 matcher 表,命中则 `where(field, '=', kw)` 并**短路**;
85
+ - `when` 是 RegExp → `pattern.test(kw)` 命中则 `where(field, '=', kw)` 并**短路**;
86
+ 3. 所有 rules 未命中 → 原 `columns` 的 OR `LIKE %kw%` fallback(现状行为)。
87
+
88
+ **命中即短路**:形状与列是一对一映射(手机号形状只可能精确查手机号列),不需要多规则 OR 合并。若同一列要接受多种形状(如手机号列同时收 11 位手机号与座机号),声明多条 rule 指向同列即可。
89
+
90
+ **生成物**:
91
+
92
+ ```ts
93
+ import { isPhone } from '@pylonts/mock/matcher';
94
+ import type { QueryBuilder } from '@pylonts/dao';
95
+
96
+ export const userListFilter = (args: UserListFilterArgs) =>
97
+ (q: QueryBuilder) => {
98
+ if (args.keyword) {
99
+ if (isPhone(args.keyword)) {
100
+ q.where('user.phone', '=', args.keyword);
101
+ } else if (/^ORD\d+$/.test(args.keyword)) {
102
+ q.where('order.order_no', '=', args.keyword);
103
+ } else {
104
+ q.where(function (this: QueryBuilder) {
105
+ this.where('user.username', 'like', `%${args.keyword}%`);
106
+ });
107
+ }
108
+ }
109
+ return q;
110
+ };
111
+ ```
112
+
113
+ - 内置语义名 → 产物 import `@pylonts/mock` 的 matcher 纯函数(`isPhone` 等),不内联正则——形状知识单点维护,与生成器共享常量;
114
+ - 项目正则 → 产物内联 `pattern.test(kw)`(`RegExp.toString()` 直出字面量,每次调用新建,无 `lastIndex` 状态问题);
115
+ - 精确查询走 `eq`(`=`),值经 knex 参数绑定,无注入面。
116
+
117
+ ## 3. 内置语义 matcher 注册表(@pylonts/mock)
118
+
119
+ **依赖方向**:mock 已是纯 JS 无依赖包,api 项目产物可直接 import。matcher 与 generator 同文件维护,形状常量共享:
120
+
121
+ ```
122
+ pylon-mock/src/generators/phone.ts // mockPhone(生成) + isPhone(识别),共享 MOBILE_PREFIXES 等常量
123
+ pylon-mock/src/generators/id.ts // mockMerchantId/mockOrderId + isMerchantId/isOrderId
124
+ pylon-mock/src/generators/identity.ts // + isIdCard
125
+ pylon-mock/src/generators/finance.ts // + isBankCard
126
+ pylon-mock/src/matcher.ts // 注册表 + 导出(子路径 '@pylonts/mock/matcher')
127
+ ```
128
+
129
+ **注册表**(mirror 现有 MockRegistry 风格,`name → matcher fn`):
130
+
131
+ | 语义名 | 判定 | 对应生成器 |
132
+ |--------|------|-----------|
133
+ | `phone` | 11 位、前缀在白名单段 | `mockPhone` |
134
+ | `merchantId` | `M` 前缀 + 数字 | `mockMerchantId` |
135
+ | `orderId` | `ORD` 前缀 + 数字 | `mockOrderId` |
136
+ | `idcard` | 18 位 / 15 位校验 | `mockIdCard` |
137
+ | `bankcard` | Luhn 校验 | `mockBankCard` |
138
+
139
+ 名单在 dsl 侧也需要可见(声明期校验语义名是否存在)。dsl 不依赖 mock(依赖方向不成立),处理:
140
+
141
+ - **方案 A**:dsl 硬编码语义名单(`KEYWORD_MATCHERS = ['phone', 'merchantId', ...] as const`),defineFilter 校验名字 ∈ 名单;mock 的注册表与之对齐(mock 侧的名单即权威实现)。dsl 只持"名字",实现永远在 mock。
142
+ - **方案 B**:dsl 不校验,gen loadFilters 校验(查 mock 注册表)——校验时机后移,声明错误要到生成才发现。
143
+
144
+ **推荐 A**:`defineFilter` 当场报错(与 conditions/keyword 非空校验一致),名单不长且是封闭枚举。mock 已依赖 `@pylonts/dsl`(^1.1.5),注册表 `Record<语义名, (s: string) => boolean>` 可直接用 dsl 导出的名单类型驱动,名单一处定义。
145
+
146
+ ## 4. 端点与前后端一致性(不变量)
147
+
148
+ - **keyword 端点不变**:`query(keyword)` 统一入口,响应类型仍 `Row[]`,分页/tenant 语义不变;
149
+ - **前端无感知**:判定全在后端 filter 产物,前端搜索框仍只传一个字符串——"前后一样"由生成链路保证(页面 search model、DTO、controller、service、DAO 全部不变,只换 filter 产物内部行为);
150
+ - **conditions 不变**:rules 命中与否,声明的 AND 条件仍照常拼接。
151
+
152
+ ## 5. 校验与错误
153
+
154
+ | 校验点 | 时机 | 行为 |
155
+ |--------|------|------|
156
+ | 语义名 ∈ 内置名单 | `defineFilter` | 抛错(与 conditions/keyword 非空校验同风格) |
157
+ | `rules` 非空 | `defineFilter` | 空数组等价于不声明,不报错 |
158
+ | 正则无 `g`/`y` 标志 | `defineFilter` | 抛错(带全局标志的 RegExp 有 lastIndex 状态,test 结果不确定) |
159
+ | rule.field 跨表 | `loadFilters` | 复用现有跨表 filter 机制(usage-site JOIN),不新增规则 |
160
+ | 语义名注册表漂移 | mock 单测 | 名单与实现一一对应(缺实现/多名均报错) |
161
+
162
+ ## 6. 安全
163
+
164
+ - **SQL**:精确查询值与 LIKE 值同走 knex 参数绑定,无拼接;
165
+ - **ReDoS**:内置 matcher 是库作者维护的白名单正则(无嵌套量词陷阱);项目自定义正则由 schema 作者负责,文档提示避免 `(a+)+` 类回溯炸弹(只 `test` 用户输入,恶意输入可触发正则回溯——风险与任何服务端正则相同);
166
+ - **无执行面**:matcher 只返回 boolean,无回调 / 无用户代码注入。
167
+
168
+ ## 7. 分阶段落地(未开始)
169
+
170
+ | 阶段 | 内容 | 产出 |
171
+ |------|------|------|
172
+ | 1 | dsl:`KeywordRule` + `keyword.rules` 声明 + defineFilter 校验(语义名单、正则标志) | `dsl/src/filter.ts` + 测试 |
173
+ | 2 | mock:matcher 注册表(phone/merchantId/orderId 起步,与生成器共享形状常量)+ 子路径导出 | `pylon-mock/src/matcher.ts` + 测试 |
174
+ | 3 | gen:keyword 块渲染 if/else-if 链(语义名 → import matcher;正则 → 内联 test;fallback → 现有 OR like) | `gen/src/filter-render.ts` + 测试 |
175
+ | 4 | examples:user-list.filter 加 `{ field: phone, when: 'phone' }`,验证产物与 api tsc | examples 回归 |
176
+
177
+ ## 8. 待决策问题
178
+
179
+ 1. **内置语义名单范围**:起步只做 `phone`(最常见)还是 phone/merchantId/orderId 三件套?身份证/银行卡的校验规则重(校验位),是否首期引入?
180
+ 2. **matcher 实现归属**:`@pylonts/mock` 是"Mock 数据生成器"定位,形状识别放这里是否符合包边界?备选是新建 `@pylonts/matcher` 或放 `@pylonts/core`(框架无关标准层,依赖方向更干净,但形状常量与 mock 生成器要共享——共享方式:mock 依赖 core,或 matcher 独立包被 mock 依赖)。
181
+ 3. **命中后是否允许继续 like**:本方案短路(命中即纯精确)。备选:命中规则后仍追加 OR like 扩大召回(多返回行),由 schema 作者选择。首期短路即可,若业务要"模糊召回"可后续加 `rule.fallbackLike?: boolean`。
182
+ 4. **精确查询的操作符**:本方案固定 `eq`。订单号/商户号等唯一键列 eq 合理;若出现"前缀精确"需求(如输入 `ORD17` 查今年订单),是否允许 rule 声明 `op: 'like'`(精确列上的前缀 LIKE)?
package/docs/project.md CHANGED
@@ -3,22 +3,30 @@
3
3
  Project 是仓库的地图:描述有哪些前端应用、哪些后端 API,以及每个 API 服务哪些前端。
4
4
 
5
5
  ```ts
6
- import { defineProject } from '@pylonts/dsl';
6
+ import { defineProject, FrontAppSchema, ProjectApiSchema } from '@pylonts/dsl';
7
7
 
8
- const webAdmin = { name: 'web-admin', type: 'admin', dir: 'web-admin/', description: '管理后台' };
9
- const miniUser = { name: 'mini-user', type: 'wxmini', dir: 'mini-user/', description: 'C端小程序' };
10
- const miniVerify = { name: 'mini-verify', type: 'wxmini', dir: 'mini-verify/',description: '核销小程序' };
8
+ // Export-symbol rule: the export name equals the schema name kebab-camel
9
+ // (name 'admin' exports 'admin', 'mini-user' exports 'miniUser').
10
+ export const admin: FrontAppSchema = { name: 'admin', type: 'admin', dir: 'admin/', description: '管理后台' };
11
+ export const miniUser: FrontAppSchema = { name: 'mini-user', type: 'wxmini', dir: 'mini-user/', description: 'C端小程序' };
12
+ export const miniVerify: FrontAppSchema = { name: 'mini-verify', type: 'wxmini', dir: 'mini-verify/', description: '核销小程序' };
13
+
14
+ // The first api must be named 'api'; a second api may use a prefixed name
15
+ // like 'xx-api' with dir 'xx-api/'.
16
+ export const api: ProjectApiSchema = { name: 'api', description: '商城主后端', dir: 'api/', contextPath: '/mall', apps: [admin, miniUser, miniVerify] };
11
17
 
12
18
  export const mall = defineProject('mall', {
13
19
  description: '合作商户权益兑换商城',
14
- apps: [webAdmin, miniUser, miniVerify],
15
- apis: [
16
- { name: 'mall-api', description: '商城主后端', dir: 'api/', contextPath: '/mall', apps: [webAdmin, miniUser, miniVerify] },
17
- ],
20
+ apps: [admin, miniUser, miniVerify],
21
+ apis: [api],
18
22
  });
19
23
  ```
20
24
 
21
25
  - `FrontAppSchema`:`name` / `description` / `type`(admin | wxmini)/ `dir`(相对仓库根目录的源码目录)。
22
26
  - `ProjectApiSchema`:`name` / `description` / `dir` / `apps`(直接引用共享的 FrontAppSchema 实例——一个 app 被多个 API 服务就定义一次、引用多次)/ `contextPath`(API 基础 URL 前缀,如 `/mall`,空串表示无前缀)。
23
27
  - **直接对象引用优先**:`api.apps` 与 `project.apps` 指向同一实例,不写字符串。
24
- - **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
28
+ - **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
29
+ - **命名约定(defineProject 运行时强制,违反即抛错)**:
30
+ - 每个 app / api / thirdApi 的 `dir` 必须等于 `name`(尾斜杠可有可无,`'api/'` == `'api'`)。
31
+ - `apis` 的第一个 api 必须命名为 `api`(导出符号即 `api`);带前缀的名字(如 `xx-api`)只允许从第二个 api 起。
32
+ - 实例导出符号 = 名字的 kebab-camel(`mini-user` → `miniUser`),loader 强制。