@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.
- package/README.md +4 -0
- package/dist/action.d.ts +32 -0
- package/dist/action.js +14 -0
- package/dist/aggregate.d.ts +38 -0
- package/dist/aggregate.js +46 -0
- package/dist/business-flow.d.ts +9 -0
- package/dist/business-flow.js +72 -0
- package/dist/controller.d.ts +17 -9
- package/dist/controller.js +8 -2
- package/dist/convert.d.ts +28 -10
- package/dist/convert.js +16 -5
- package/dist/curd.d.ts +7 -10
- package/dist/curd.js +3 -1
- package/dist/dao.d.ts +81 -53
- package/dist/dao.js +291 -12
- package/dist/db.d.ts +6 -0
- package/dist/db.js +10 -0
- package/dist/domain-event.d.ts +48 -0
- package/dist/domain-event.js +24 -0
- package/dist/dsl.d.ts +17 -2
- package/dist/dsl.js +7 -0
- package/dist/dto.d.ts +6 -4
- package/dist/dto.js +5 -4
- package/dist/entity.d.ts +29 -0
- package/dist/entity.js +13 -0
- package/dist/exception.d.ts +9 -3
- package/dist/exception.js +25 -1
- package/dist/expr.d.ts +45 -0
- package/dist/expr.js +32 -0
- package/dist/filter.d.ts +45 -0
- package/dist/filter.js +21 -0
- package/dist/flow-script.d.ts +108 -0
- package/dist/flow-script.js +505 -0
- package/dist/flow.d.ts +294 -17
- package/dist/flow.js +803 -18
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/mermaid-driver.js +264 -24
- package/dist/mysql-driver.js +3 -0
- package/dist/project.d.ts +10 -6
- package/dist/project.js +35 -4
- package/dist/repository.d.ts +26 -0
- package/dist/repository.js +8 -0
- package/dist/service.d.ts +14 -2
- package/dist/service.js +49 -0
- package/dist/third-service.d.ts +5 -0
- package/dist/third-service.js +1 -0
- package/dist/typebox-driver.js +4 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +4 -0
- package/docs/aggregate.md +110 -0
- package/docs/curd.md +146 -111
- package/docs/dao-generation.md +478 -0
- package/docs/ddd-principles.md +75 -0
- package/docs/domain-event.md +137 -0
- package/docs/keyword-matcher.md +182 -0
- package/docs/project.md +17 -9
- package/docs/token.md +327 -0
- package/docs/trans-reentrant.md +85 -0
- package/package.json +25 -6
- package/src/action.ts +51 -10
- package/src/aggregate.ts +104 -0
- package/src/business-flow.ts +80 -0
- package/src/controller.ts +25 -11
- package/src/convert.ts +51 -15
- package/src/curd.ts +12 -6
- package/src/dao.ts +377 -63
- package/src/db.ts +13 -0
- package/src/domain-event.ts +74 -0
- package/src/dsl.ts +23 -2
- package/src/dto.ts +9 -6
- package/src/entity.ts +43 -0
- package/src/exception.ts +30 -5
- package/src/expr.ts +65 -0
- package/src/filter.ts +70 -0
- package/src/flow-script.ts +696 -0
- package/src/flow.ts +1129 -46
- package/src/index.ts +6 -2
- package/src/mermaid-driver.ts +256 -29
- package/src/mysql-driver.ts +3 -0
- package/src/project.ts +138 -97
- package/src/repository.ts +35 -0
- package/src/service.ts +68 -3
- package/src/third-service.ts +6 -0
- package/src/typebox-driver.ts +4 -0
- package/src/utils.ts +13 -2
- package/src/endpoint.ts +0 -18
- package/src/provider.ts +0 -68
package/dist/third-service.js
CHANGED
package/dist/typebox-driver.js
CHANGED
|
@@ -62,6 +62,10 @@ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
|
|
|
62
62
|
}
|
|
63
63
|
return `Type.Enum(${ref.name})`;
|
|
64
64
|
}
|
|
65
|
+
case 'aggregate':
|
|
66
|
+
// Aggregate query outputs: count/sum(int) are numbers, everything else
|
|
67
|
+
// arrives as a precision string.
|
|
68
|
+
return field.jsType === 'number' ? 'Type.Number()' : 'Type.String()';
|
|
65
69
|
case 'array':
|
|
66
70
|
return `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
|
|
67
71
|
case 'object':
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Field, SchemaBase } from './dsl.js';
|
|
2
|
-
import type { FrontAppSchema } from './project.js';
|
|
2
|
+
import type { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
3
3
|
/** A utility method with a full signature. */
|
|
4
4
|
export interface UtilsMethodSchema extends SchemaBase {
|
|
5
5
|
type: 'utilsMethod';
|
|
@@ -15,13 +15,20 @@ export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>
|
|
|
15
15
|
/** A base utility module (e.g. DateTimeUtils). */
|
|
16
16
|
export interface UtilsSchema extends SchemaBase {
|
|
17
17
|
type: 'utils';
|
|
18
|
-
/**
|
|
18
|
+
/** Backend binding — the api module this utils belongs to (shared instance
|
|
19
|
+
* from project.config.ts apis). With `app` it serves that frontend
|
|
20
|
+
* ({api}/{app}/utils/); alone it is the api's public module
|
|
21
|
+
* ({api}/common/utils/). Unset = not backend-side. */
|
|
22
|
+
api?: ProjectApiSchema;
|
|
23
|
+
/** Optional binding — the frontend app this utils serves (shared instance
|
|
24
|
+
* from project.config.ts apps). Empty means a shared public module. */
|
|
19
25
|
app?: FrontAppSchema;
|
|
20
26
|
/** Methods keyed by name — the map key is written back as the method name. */
|
|
21
27
|
methods: Record<string, UtilsMethodSchema>;
|
|
22
28
|
}
|
|
23
29
|
export declare function defineUtils(options: {
|
|
24
30
|
name: string;
|
|
31
|
+
api?: ProjectApiSchema;
|
|
25
32
|
app?: FrontAppSchema;
|
|
26
33
|
methods: Record<string, UtilsMethodDef>;
|
|
27
34
|
description?: string;
|
package/dist/utils.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export function defineUtils(options) {
|
|
2
|
+
if (options.api !== undefined && options.app !== undefined && !options.api.apps.includes(options.app)) {
|
|
3
|
+
throw new Error(`utils ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
4
|
+
}
|
|
2
5
|
const schema = {
|
|
3
6
|
type: 'utils',
|
|
4
7
|
name: options.name,
|
|
5
8
|
description: options.description,
|
|
9
|
+
api: options.api,
|
|
6
10
|
app: options.app,
|
|
7
11
|
methods: {},
|
|
8
12
|
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 聚合与仓储 DSL 扩展规划(Aggregate / Repository)
|
|
2
|
+
|
|
3
|
+
> 状态:**规划中(未实现)**
|
|
4
|
+
> 关联代码:`dsl/src/db.ts`(TableSchema)、`dsl/src/dao.ts`(DaoSchema)、`dsl/src/service.ts`(ServiceSchema)
|
|
5
|
+
> 背景对话:ts-libs 会话「dd DDD 扩展讨论」(商城下单用例)
|
|
6
|
+
|
|
7
|
+
## 1. 背景:pylon 现状与 DDD 缺口
|
|
8
|
+
|
|
9
|
+
**现状**:
|
|
10
|
+
|
|
11
|
+
- `TableSchema` 是全局扁平数据字典("表无物理归属"),orders / order_items 是两张互相独立声明、无聚合关系的表;
|
|
12
|
+
- `DaoSchema` 是单表单 SQL(knex 透明代理),粒度 = 表,无跨表能力;
|
|
13
|
+
- 多表读写由 service flow 手工编排:`dao.insert(orders) → dao.insert(order_items)` + 手工 `@Trans()`,"总额 = Σ明细"这类一致性靠开发者自觉,无强制约束。
|
|
14
|
+
|
|
15
|
+
**DDD 缺口对照**(战术模式):
|
|
16
|
+
|
|
17
|
+
| DDD 概念 | dsl 现状 | 缺口 |
|
|
18
|
+
|---------|---------|------|
|
|
19
|
+
| 聚合 / 聚合根 | 无 | **全新概念** |
|
|
20
|
+
| 值对象 | mock 识别"金额/手机号"语义 | 无声明层 |
|
|
21
|
+
| 领域事件 | 只有 UI 组件事件(event.ts) | **全新概念** |
|
|
22
|
+
| 领域服务 | 全混在应用服务(service_schema) | 需拆分 |
|
|
23
|
+
| 仓储 | 无(DAO 粒度是表) | **全新概念** |
|
|
24
|
+
| 应用服务编排 | service_schema + flow ✅ | 已具备 |
|
|
25
|
+
| 防腐层 | ThirdServiceSchema 只隔离 | 缺模型映射 |
|
|
26
|
+
|
|
27
|
+
## 2. 扩展一:AggregateSchema(核心)
|
|
28
|
+
|
|
29
|
+
**作用**:显式声明"哪些表属于同一个聚合、谁是聚合根、成员如何挂载、跨成员不变式、聚合间引用规则"。这是把"多表一致性从约定变约束"的落点。
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
defineAggregate({
|
|
33
|
+
name: 'Order',
|
|
34
|
+
root: ordersTable, // 聚合根表
|
|
35
|
+
members: { // 成员表("包含几个 table schema" 的声明)
|
|
36
|
+
items: { table: orderItemsTable, via: 'order_no' }, // 1:N,外键挂根
|
|
37
|
+
address: { table: orderAddressTable, via: 'order_no', one: true }, // 1:1
|
|
38
|
+
},
|
|
39
|
+
invariants: [ // 跨成员表不变式,挂聚合上,可被生成代码消费
|
|
40
|
+
{ name: 'total = sum(items.price * items.qty)',
|
|
41
|
+
check: 'totalAmount == sum(items.price * items.qty)' },
|
|
42
|
+
],
|
|
43
|
+
references: { productId: 'ProductAggregate' }, // 聚合间只按 ID 引用
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| 声明项 | 含义 | 缺了会怎样 |
|
|
48
|
+
|--------|------|-----------|
|
|
49
|
+
| `root` | 谁是聚合根 | 分不清一致性入口 |
|
|
50
|
+
| `members` | 包含哪几个 table schema | "包含"只是列表,无结构 |
|
|
51
|
+
| `members[i].via` | 成员表靠哪个外键挂根 | 工具无法推导 join / 级联关系 |
|
|
52
|
+
| `invariants` | 跨成员一致性规则 | "总额=Σ明细"又回到 flow 里手工写 |
|
|
53
|
+
| `references` | 聚合间只按 ID 引用 | 无法 lint 跨聚合直接持表引用 |
|
|
54
|
+
|
|
55
|
+
**消费方**(声明一旦存在即可自动推导):
|
|
56
|
+
|
|
57
|
+
1. **生成 Repository**:按"加载 / 保存 / 删除"三套固定骨架自动产出(见下),`orders + order_items` 自动同事务,不再手工 `@Trans()`;
|
|
58
|
+
2. **lint 约束**:禁止聚合外代码直接 `insert/update/delete` 成员表(只准经 root 走);
|
|
59
|
+
3. **不变式挂载**:save 前后强制校验。
|
|
60
|
+
|
|
61
|
+
**多表映射三种模式**(聚合↔表):A 单表=单聚合(1:1,几乎透明);B 一聚合=多表(1:N,最常见,Order 案例);C 多聚合共享表(N:1,DDD 不推荐)。
|
|
62
|
+
|
|
63
|
+
## 3. 扩展二:RepositorySchema(可推导,也可显式声明)
|
|
64
|
+
|
|
65
|
+
**作用**:聚合粒度的存储入口,把"哪些表一起查、怎么拼成聚合"的知识从 service flow 下沉到仓储。调用方只面对领域概念(Order),不面对表。
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
defineRepository({
|
|
69
|
+
name: 'OrderRepository',
|
|
70
|
+
aggregate: orderAggregate, // 绑定聚合
|
|
71
|
+
// 内部如何落到 DAO 由 generator 按 aggregate 结构自动展开:
|
|
72
|
+
// save(order) = tx { ordersDao.upsert + orderItemsDao 级联 }
|
|
73
|
+
// findByOrderNo() = ordersDao.get + orderItemsDao 按 order_no 查
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**聚合内 join 下沉、聚合间禁止 join**:
|
|
78
|
+
|
|
79
|
+
- 聚合内:加载整个 Order(根 + 明细 + 地址)由 Repository 内部完成,调用方一行,join 知识声明在 `members[].via`;
|
|
80
|
+
- 聚合间:只按 ID 引用,跨聚合 join 是 DDD 禁止的;展示商品名这类信息走读模型 / 查询服务(CQRS 的 Q 侧),或应用服务分步查 + 内存组装。
|
|
81
|
+
|
|
82
|
+
**分层对照**:
|
|
83
|
+
|
|
84
|
+
| 层 | 操作单元 | 一次操作覆盖 | dsl |
|
|
85
|
+
|----|---------|------------|-----|
|
|
86
|
+
| Service | 用例 | 跨多个聚合/服务 | service_schema ✅ |
|
|
87
|
+
| Repository | **聚合** | orders + order_items 一个事务 | **本扩展** |
|
|
88
|
+
| DAO | **表** | 单表一条 SQL | dao_schema ✅ |
|
|
89
|
+
|
|
90
|
+
## 4. 扩展三:引入支持 DDD 的 TS 库(选型,待决策)
|
|
91
|
+
|
|
92
|
+
dsl 是**声明期**(`defineAggregate` 声明结构),引入的库是**运行期**(代码跑起来时持久化/发事件)。两者互补,不冲突——声明可翻译为运行期库的配置。
|
|
93
|
+
|
|
94
|
+
| 库 | 类型 | 聚合能力 | 与本扩展关系 |
|
|
95
|
+
|----|------|---------|-------------|
|
|
96
|
+
| **MikroORM** | ORM | Entity / Repository / Unit of Work / Identity Map / cascade persist | **首选参考**:`@OneToMany(cascade, orphanRemoval)` + `em.persist(order)` 就是"orders + order_items 同事务整体落库"的标准实现;AggregateSchema 声明可翻译成它的映射配置 |
|
|
97
|
+
| **Remesh** | DDD 框架 | CQRS + 领域事件 + Command/Query 分离 | 领域事件 / CQRS 参考 |
|
|
98
|
+
| **Emmett** | 事件溯源 | 聚合状态由事件重建 | 事件溯源聚合参考 |
|
|
99
|
+
| TypeORM | ORM | Repository + cascade,无 UoW / Identity Map | 弱支持,不推荐 |
|
|
100
|
+
| Prisma / Drizzle | 查询构建器 | 不支持聚合 | 需手工包 Repository |
|
|
101
|
+
|
|
102
|
+
**建议**:运行时持久化参考/选用 **MikroORM**(聚合持久化设计最完整);领域事件参考 **Remesh**。dsl 的 `AggregateSchema` 声明层本身无现成开源,属本项目的增量设计空间。
|
|
103
|
+
|
|
104
|
+
## 5. 落地步骤(待办,未开工)
|
|
105
|
+
|
|
106
|
+
1. `AggregateSchema` 类型 + `defineAggregate` + 定义期校验(root 必须在其表内、via 外键存在、成员表不能是其他聚合的 root 等);
|
|
107
|
+
2. `RepositorySchema` 类型 + `defineRepository`(或从 aggregate 自动推导生成);
|
|
108
|
+
3. `gen` 生成 Repository 代码:加载/保存/删除三套骨架 + 同事务包装(`@Trans()` 从声明推导);
|
|
109
|
+
4. `lint` 聚合边界检查:聚合外禁止直接改成员表、聚合间禁止跨表引用;
|
|
110
|
+
5. 运行期库选型落地(MikroORM 或保持 DAO 同事务包装)。
|
package/docs/curd.md
CHANGED
|
@@ -1,111 +1,146 @@
|
|
|
1
|
-
# 管理端 CRUD 页面标准(CurdSchema)
|
|
2
|
-
|
|
3
|
-
CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD 页面标准:绑定一张实体表 + 一个管理端 app,描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页(+ 动作页)的生成规格。
|
|
4
|
-
|
|
5
|
-
页面定义文件按实体组织:`{project}/pages/{entity}.curd.ts`(与 `schema/*.table.ts` 平级)。
|
|
6
|
-
|
|
7
|
-
**CurdSchema 只依赖 table schema(`Field` 实例),不挂钩 DTO(`DtoMessage`)**——DTO 由生成器按标准从 `columns` 推导。
|
|
8
|
-
|
|
9
|
-
## 定义
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { defineCurd } from '@pylonts/dsl';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
columns: [order.columns.id, order.columns.order_no
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1
|
+
# 管理端 CRUD 页面标准(CurdSchema)
|
|
2
|
+
|
|
3
|
+
CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD 页面标准:绑定一张实体表 + 一个管理端 app,描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页(+ 动作页)的生成规格。
|
|
4
|
+
|
|
5
|
+
页面定义文件按实体组织:`{project}/pages/{entity}.curd.ts`(与 `schema/*.table.ts` 平级)。
|
|
6
|
+
|
|
7
|
+
**CurdSchema 只依赖 table schema(`Field` 实例),不挂钩 DTO(`DtoMessage`)**——DTO 由生成器按标准从 `columns` 推导。
|
|
8
|
+
|
|
9
|
+
## 定义
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineCurd } from '@pylonts/dsl';
|
|
13
|
+
import { admin } from '../project.config';
|
|
14
|
+
import { order } from '../schema/order.table';
|
|
15
|
+
import { merchant } from '../schema/merchant.table';
|
|
16
|
+
import { orderListFilter } from '../filter_schema/api/admin/filter/order-list.filter';
|
|
17
|
+
|
|
18
|
+
export const orderCurd = defineCurd('order', { // name = table.name 的 kebab(即 admin 路由路径)
|
|
19
|
+
description: '订单管理',
|
|
20
|
+
app: admin, // 所属管理端(project.config.ts 的 FrontAppSchema 共享实例)
|
|
21
|
+
table: order, // 绑定实体表(共享实例)
|
|
22
|
+
title: '订单管理',
|
|
23
|
+
section: '订单管理', // 必填:sidebar 分组名
|
|
24
|
+
actions: [defineAction('EXPORT', '导出订单')], // 额外操作按钮
|
|
25
|
+
actionPages: {
|
|
26
|
+
add: { mode: 'modal', columns: [order.columns.order_no, order.columns.mer_id] },
|
|
27
|
+
update: { mode: 'modal', columns: [order.columns.id, order.columns.order_no] },
|
|
28
|
+
detail: { mode: 'route', columns: [order.columns.id, order.columns.order_no, order.columns.amount] },
|
|
29
|
+
},
|
|
30
|
+
list: {
|
|
31
|
+
columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 可跨表
|
|
32
|
+
filter: orderListFilter, // 搜索表单 + keyword(FilterSchema 引用,可选)
|
|
33
|
+
orderBy: { column: order.columns.id, direction: 'desc' },
|
|
34
|
+
columnTitles: { order_no: '订单号', name: '商户名称' }, // Field.name → 文案
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
搜索条件**不内联在 list 里**,而是独立的 **FilterSchema**(`defineFilter`)声明,存放在 `filter_schema/{api.name}/{app.name}/filter/`(机器校验:一文件一 filter,文件名 = 名字去 Filter 后缀转 kebab):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// filter_schema/api/admin/filter/order-list.filter.ts
|
|
43
|
+
import { defineFilter } from '@pylonts/dsl';
|
|
44
|
+
import { admin, api } from '../../../project.config';
|
|
45
|
+
import { order } from '../../../schema/order.table';
|
|
46
|
+
|
|
47
|
+
export const orderListFilter = defineFilter({
|
|
48
|
+
name: 'OrderListFilter',
|
|
49
|
+
api,
|
|
50
|
+
app: admin,
|
|
51
|
+
conditions: [
|
|
52
|
+
{ field: order.columns.status, optional: true }, // op 默认 eq;optional = 有值才加 WHERE
|
|
53
|
+
{ field: order.columns.order_no, op: 'like', optional: true },
|
|
54
|
+
],
|
|
55
|
+
keyword: { columns: [order.columns.order_no] }, // 单输入值多列 OR 模糊
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
生成物为 `{api}/src/modules/{app}/filter/{FilterName}.ts`(两段柯里化 WHERE 拼装方法,DAO/Service 列表查询共用)。
|
|
60
|
+
|
|
61
|
+
## 字段
|
|
62
|
+
|
|
63
|
+
| 字段 | 类型 | 说明 |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `app` | `FrontAppSchema` | 所属管理端(共享实例,`type` 必须为 `'admin'`) |
|
|
66
|
+
| `table` | `TableSchema` | 绑定实体表(共享实例) |
|
|
67
|
+
| `title` | `string` | 列表页中文标题 |
|
|
68
|
+
| `section` | `string` | **必填**:sidebar 分组名 |
|
|
69
|
+
| `actions?` | `ActionSchema[]` | 页面额外可执行动作(标准 CRUD 之外,如导出、审核) |
|
|
70
|
+
| `actionPages?` | `{ add? / update? / detail? }` | 动作页:`{ mode: 'modal' \| 'route'; columns: Field[] }` |
|
|
71
|
+
| `list` | `CurdListConfig` | 列表页配置(必填) |
|
|
72
|
+
|
|
73
|
+
### ActionPage
|
|
74
|
+
|
|
75
|
+
| 字段 | 类型 | 说明 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `mode` | `'modal' \| 'route'` | 弹窗或独立路由 |
|
|
78
|
+
| `columns` | `Field[]` | 该页面渲染的字段,**必填非空**——前端要显示的字段必须全部显式列出 |
|
|
79
|
+
|
|
80
|
+
### CurdListConfig
|
|
81
|
+
|
|
82
|
+
| 字段 | 类型 | 说明 |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `columns` | `Field[]` | 列表列,**必填非空**;可含跨表字段 |
|
|
85
|
+
| `filter?` | `FilterSchema` | 页面过滤器引用:搜索表单(AND 条件)+ keyword(多列 OR 模糊);缺省 = 无搜索表单 |
|
|
86
|
+
| `orderBy` | `{ column: Field; direction: 'asc' \| 'desc' }` | 默认排序,**必填**,column 与 direction 都必填;column 必须是**本表字段实例** |
|
|
87
|
+
| `columnTitles?` | `Record<string, string>` | 列标题覆盖:`Field.name` → 中文文案 |
|
|
88
|
+
|
|
89
|
+
### FilterSchema(`defineFilter`)
|
|
90
|
+
|
|
91
|
+
| 字段 | 类型 | 说明 |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `name` | `string` | PascalCase、`Filter` 结尾;导出名 = name 首字母小写 |
|
|
94
|
+
| `api` | `ProjectApiSchema` | 所属后端 api(project.config.ts 共享实例);`api.apps` 必须包含 `app` |
|
|
95
|
+
| `app` | `FrontAppSchema` | 所属前端 app(共享实例);必须与引用它的 curd 同 app |
|
|
96
|
+
| `conditions?` | `FilterCondition[]` | AND 组合条件:`{ field, op?='eq', right?, optional? }`;`optional: true` = 有值才加 WHERE(页面搜索场景) |
|
|
97
|
+
| `keyword?` | `{ columns: Field[] }` | 单输入值对多列 OR like 模糊;配置后驱动「关键词查询」端点(`query({ keyword })`,供 Select/AutoComplete 搜索) |
|
|
98
|
+
|
|
99
|
+
## 跨表字段
|
|
100
|
+
|
|
101
|
+
`list.columns` 与 filter `conditions` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示/过滤关联表字段(如订单列表显示商户名称、按商户名称过滤)。
|
|
102
|
+
|
|
103
|
+
## 默认与校验
|
|
104
|
+
|
|
105
|
+
- `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)
|
|
106
|
+
- `list.orderBy` **必填**,`column` 与 `direction` 都必填(规格:默认主键 desc 由定义方显式写出)
|
|
107
|
+
- 运行时校验(`defineCurd`,仿 `defineTable` 强校验风格):
|
|
108
|
+
- `app.type` 必须为 `'admin'`,否则抛错
|
|
109
|
+
- **`name` 必须是 `table.name` 的 kebab 形式**(name 即 admin 路由路径,不允许与所服务的表漂移)
|
|
110
|
+
- `section` 必填
|
|
111
|
+
- 所有 `columns` 非空,否则抛错
|
|
112
|
+
- `list.filter.app` 必须 === `curd.app`,否则抛错
|
|
113
|
+
- `list.orderBy.column` 必须属于 `table`,否则抛错
|
|
114
|
+
- `list.columns` 允许跨表,**不校验归属**
|
|
115
|
+
- 运行时校验(`defineFilter`):`api.apps` 包含 `app`;conditions 与 keyword 不能同时为空;keyword.columns 非空
|
|
116
|
+
- 生成时校验(curd 生成器,`DtoSchemaGen.add` / `update`):
|
|
117
|
+
- 表配置 `autoIncrement` 或 `generator`(主键由服务端生成)时,`actionPages.add.columns` **不允许包含主键字段**,否则抛错——AddRequest 不携带服务端生成的主键
|
|
118
|
+
- `actionPages.update.columns` **必须包含主键字段**,否则抛错——UpdateRequest 靠主键定位记录
|
|
119
|
+
|
|
120
|
+
## DTO 推导(生成器约定)
|
|
121
|
+
|
|
122
|
+
DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导,页面语义不持有 DTO 实例:
|
|
123
|
+
|
|
124
|
+
| DTO | 命名 | 字段来源 |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Row | `{Pascal}Row` | `list.columns` |
|
|
127
|
+
| ListRequest | `{Pascal}ListRequest` | filter 的 conditions(camelCase + op)与 keyword + 分页参数(`PageRequest`,仅 paginated 表) |
|
|
128
|
+
| QueryRequest | `{Pascal}QueryRequest` | filter 的 conditions + keyword,无分页——keyword 查询端点专用(仅配置 keyword 时生成) |
|
|
129
|
+
| ListResponse | `{Pascal}ListResponse` | `PageResult(Row)`(仅 paginated 表;非分页表列表接口直接返回 `Row[]`,不生成 ListResponse) |
|
|
130
|
+
| AddRequest | `{Pascal}AddRequest` | `actionPages.add.columns` |
|
|
131
|
+
| UpdateRequest | `{Pascal}UpdateRequest` | `actionPages.update.columns` |
|
|
132
|
+
| DetailRequest | `{Pascal}DetailRequest` | 主键 |
|
|
133
|
+
| DetailResponse | `{Pascal}DetailResponse` | `actionPages.detail.columns` |
|
|
134
|
+
|
|
135
|
+
## 与旧 PageConfig 的差异
|
|
136
|
+
|
|
137
|
+
| PageConfig(旧方案,已废弃) | CurdSchema |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `module: string` | 由 `app` 推导(后端模块 == app 1:1) |
|
|
140
|
+
| `schema: 'bd'` 字符串 | `table: TableSchema` 实例(类型安全) |
|
|
141
|
+
| `operations: { label, action }` | `actions: ActionSchema[]` |
|
|
142
|
+
| `detail.mode` 单例 | `actionPages.detail.mode` |
|
|
143
|
+
| `forms.add / forms.update` | `actionPages.add / actionPages.update` |
|
|
144
|
+
| `keyword` / `orderBy` / `columnTitles` | `list.filter`(FilterSchema)/ `list.orderBy` / `list.columnTitles` |
|
|
145
|
+
| `naming` | 去掉(DTO 命名是生成器约定,非页面语义) |
|
|
146
|
+
| DTO 引用(`request` / `fields` / `DtoFields`) | 去掉(DTO 由生成器推导,页面只依赖 table) |
|