@pylonts/dsl 1.1.5 → 1.1.6
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/controller.d.ts +27 -0
- package/dist/controller.js +11 -0
- package/dist/convert.d.ts +18 -6
- package/dist/convert.js +12 -2
- package/dist/curd.d.ts +0 -2
- package/dist/curd.js +7 -0
- package/dist/dao.d.ts +111 -2
- package/dist/dao.js +28 -2
- package/dist/db.js +3 -0
- package/dist/dsl.d.ts +16 -1
- package/dist/dsl.js +6 -0
- package/dist/dto.d.ts +6 -2
- package/dist/dto.js +20 -9
- package/dist/endpoint.d.ts +15 -0
- package/dist/endpoint.js +3 -0
- package/dist/exception.d.ts +14 -0
- package/dist/exception.js +9 -0
- package/dist/field-rule.d.ts +20 -0
- package/dist/field-rule.js +19 -0
- package/dist/flow.d.ts +24 -2
- package/dist/flow.js +16 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/mermaid-driver.js +32 -3
- package/dist/method.d.ts +11 -0
- package/dist/method.js +3 -0
- package/dist/mysql-driver.js +4 -0
- package/dist/provider.d.ts +6 -11
- package/dist/provider.js +2 -2
- package/dist/service.d.ts +16 -6
- package/dist/service.js +13 -2
- package/dist/third-service.d.ts +75 -0
- package/dist/third-service.js +96 -0
- package/dist/typebox-driver.d.ts +6 -0
- package/dist/typebox-driver.js +73 -12
- package/dist/utils.d.ts +25 -10
- package/dist/utils.js +28 -11
- package/docs/dto.md +73 -66
- package/docs/third-service.md +122 -0
- package/package.json +4 -1
- package/src/controller.ts +40 -0
- package/src/convert.ts +42 -15
- package/src/curd.ts +98 -93
- package/src/dao.ts +172 -13
- package/src/db.ts +186 -181
- package/src/dsl.ts +26 -1
- package/src/dto.ts +263 -247
- package/src/endpoint.ts +18 -0
- package/src/exception.ts +28 -0
- package/src/field-rule.ts +47 -0
- package/src/flow.ts +143 -103
- package/src/index.ts +43 -33
- package/src/mermaid-driver.ts +112 -84
- package/src/method.ts +20 -0
- package/src/mysql-driver.ts +4 -0
- package/src/provider.ts +67 -72
- package/src/service.ts +42 -20
- package/src/third-service.ts +186 -0
- package/src/typebox-driver.ts +82 -11
- package/src/utils.ts +63 -26
- package/dist/check-inheritance.d.ts +0 -9
- package/dist/check-inheritance.js +0 -58
package/docs/dto.md
CHANGED
|
@@ -1,67 +1,74 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- `
|
|
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
|
+
- `setDefault(v)` 设 DTO 层默认值,渲染为 TypeBox `default:` 注解(`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`)。
|
|
54
|
+
- 枚举默认值必须是该枚举的成员值(value),渲染时解析为成员引用;非成员值直接报错。
|
|
55
|
+
- 未显式设置时,fallback 到字段构建器的 `default`(DB 默认值,string),`from()` 提取的字段自动带出。
|
|
56
|
+
|
|
57
|
+
## 继承基础 schema
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
buildQuery('OrderPageQuery', { ... })
|
|
61
|
+
.include({ from: '@pylonts/core', name: 'PageRequest' }); // 渲染 Type.Intersect([PageRequest, ...])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 关键语义
|
|
65
|
+
|
|
66
|
+
- **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
|
|
67
|
+
- **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
|
|
68
|
+
- **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
|
|
69
|
+
|
|
70
|
+
## 文件组织
|
|
71
|
+
|
|
72
|
+
- `dto_schema/{module}/*.dto.ts`:DTO 源文件,一个文件可导出多个 DtoMessage;`module` = `project.config.ts` 的 `apps.name`,另加 `common/` 放跨 app 共享 DTO。
|
|
73
|
+
- `pylonts gen dto` 按 module 逐个扫描:`dto_schema/{module}/*.dto.ts` → 生成 `dto/{module}/{Name}.dto.ts`。
|
|
67
74
|
- 枚举字段引用 `enums/` 下生成的枚举源文件,DTO 文件通过 `../../enums/{JsName}.enum` 导入。
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# 定义第三方服务(ThirdServiceSchema)
|
|
2
|
+
|
|
3
|
+
第三方服务适配器契约(如微信支付 tenpay、短信、文件存储)。`defineThirdService` 声明适配器类契约:构造函数配置 + 方法列表。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { defineFieldRule, defineThirdService } from '@pylonts/dsl';
|
|
7
|
+
import { wx } from '../project.config';
|
|
8
|
+
|
|
9
|
+
const balance = intField({ optional: false, label: '余额(分)' });
|
|
10
|
+
|
|
11
|
+
// Rule = name + two named ends. One rule per semantic (defining fenYuan twice throws).
|
|
12
|
+
const fenYuan = defineFieldRule({
|
|
13
|
+
name: 'fenYuan',
|
|
14
|
+
ends: { fen: {}, yuan: {} },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const wxPayService = defineThirdService({
|
|
18
|
+
schema: wx,
|
|
19
|
+
name: 'WxPayService',
|
|
20
|
+
description: '微信支付服务(tenpay APIv2)',
|
|
21
|
+
methods: [
|
|
22
|
+
{
|
|
23
|
+
name: 'queryBalance',
|
|
24
|
+
args: { name: 'QueryBalanceArgs', fields: { openid: user.columns.openid } },
|
|
25
|
+
results: {
|
|
26
|
+
name: 'QueryBalanceResult',
|
|
27
|
+
fields: {
|
|
28
|
+
balance,
|
|
29
|
+
},
|
|
30
|
+
refs: [
|
|
31
|
+
{ field: balance, ref: order.columns.amount, convert: { rule: fenYuan, end: fenYuan.ends.fen } },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ThirdMethodSchema:方法的 args / results
|
|
40
|
+
|
|
41
|
+
每个方法的 `args` / `results` 各是一个 `ThirdMethodSchema`——**字段集合容器**,与 `TableSchema.columns` 同构,但字段是线格式(wire-format)字段。结构上它是 `DtoMessage` 的字段集合对应物。
|
|
42
|
+
|
|
43
|
+
| 属性 | 说明 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| `name` | 消息名(如 `QueryBalanceResult`),生成产物的类型名 |
|
|
46
|
+
| `fields` | 线格式字段 map,key 即协议字段名(`out_trade_no`、`appId` 原样保留) |
|
|
47
|
+
| `refs` | 同事实变体链接(见下) |
|
|
48
|
+
| `schema` | 反向指针,指向所属 method(构建器写入) |
|
|
49
|
+
|
|
50
|
+
`defineThirdMethod` 写回自有字段的 `name/schema`(与 `defineTable` 同一惯例)。
|
|
51
|
+
|
|
52
|
+
## 字段与本地实体的关系
|
|
53
|
+
|
|
54
|
+
两个通道,按"同一事实"的表达方式选择:
|
|
55
|
+
|
|
56
|
+
### 同一概念且类型一致 → 共享实例
|
|
57
|
+
|
|
58
|
+
直接复用本地实体列实例,类型/语义/默认值自动跟随实体,DTO 投影继承全部语义(与 `from(table)` 完全同构):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
args: {
|
|
62
|
+
name: 'QueryBalanceArgs',
|
|
63
|
+
fields: {
|
|
64
|
+
openid: user.columns.openid, // 共享实例,schema 仍指向 t_user
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
前提是线格式字段名与实体列名一致(协议恰好也叫 `openid`)。名字不同时(协议叫 `userId` 而列叫 `user_id`),不共享实例,走 refs。
|
|
70
|
+
|
|
71
|
+
### 同一概念但类型/格式不同(变体)→ 自有字段 + refs
|
|
72
|
+
|
|
73
|
+
声明自有线格式类型,再挂一条 `refs` 链接指向实体列。规则先定义一次:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// 规则 = 名称 + 两端(具名 map)。同一语义全局只允许一条(重复定义抛错)。
|
|
77
|
+
const fenYuan = defineFieldRule({
|
|
78
|
+
name: 'fenYuan',
|
|
79
|
+
ends: { fen: {}, yuan: {} },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const totalFee = intField({ optional: false, label: '金额(分)' });
|
|
83
|
+
|
|
84
|
+
fields: {
|
|
85
|
+
total_fee: totalFee,
|
|
86
|
+
},
|
|
87
|
+
refs: [
|
|
88
|
+
{
|
|
89
|
+
field: totalFee, // 本地定义(本消息的 wire 字段)
|
|
90
|
+
ref: order.columns.amount, // 其他定义(表列或其他消息字段)
|
|
91
|
+
convert: { rule: fenYuan, end: fenYuan.ends.fen },
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `field`:本消息的 wire 字段实例(构建器校验必须是本消息字段)
|
|
97
|
+
- `ref`:其他 schema 的字段实例(表列或其他消息字段,构建器校验不得是本消息字段)
|
|
98
|
+
- `convert`(可选):绑定一条规则到这对字段——仅当两字段需要转化时声明,纯关联不需要
|
|
99
|
+
- `rule`:`FieldRuleSchema`——规则 = 名称 + 两端(如 `fenYuan` 的 `fen`/`yuan` 端)。加密/脱敏/换算统一为规则名维度,`defineFieldRule` 按名称查重,同一语义只声明一次
|
|
100
|
+
- `end`:`field` 所站的端——引用 `rule.ends.fen` / `rule.ends.yuan`(具名引用,无索引魔法;构建器按实例校验),`ref` 自动占另一端——不再重复声明 from/to
|
|
101
|
+
- 生成器将来为这对字段产出两个方向的函数(field 端→ref 端 与 ref 端→field 端)
|
|
102
|
+
|
|
103
|
+
## 嵌套字段
|
|
104
|
+
|
|
105
|
+
线格式字段支持递归嵌套,用 `arrayField` / `objectField`(Field 体系,非表列):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
fields: {
|
|
109
|
+
payer_info: objectField({
|
|
110
|
+
properties: {
|
|
111
|
+
openid: stringField({ optional: false, maxLength: 64 }),
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
coupons: arrayField({ items: intField() }),
|
|
115
|
+
},
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
表列不支持这两个类型(`buildCreateTableSql` 直接报错)。
|
|
119
|
+
|
|
120
|
+
## DTO 转发
|
|
121
|
+
|
|
122
|
+
DTO 通过 `from(thirdMethod, fields)` 投影第三方消息字段,构建转发引用(透传不复制)。线格式字段名保持协议原样,不做 camelCase。见 [dto.md](./dto.md#从字段集合投影)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonts/dsl",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
],
|
|
25
25
|
"author": "",
|
|
26
26
|
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@pylonts/core": "file:../pylon"
|
|
29
|
+
},
|
|
27
30
|
"devDependencies": {
|
|
28
31
|
"typescript": "^7.0.2",
|
|
29
32
|
"vitest": "^4.1.10"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import { FrontAppSchema } from './project.js';
|
|
3
|
+
import type { EndpointSchema } from './endpoint.js';
|
|
4
|
+
|
|
5
|
+
/** A backend RPC controller. Strong constraints:
|
|
6
|
+
* - a backend module maps 1:1 to a frontend app (they are peers);
|
|
7
|
+
* - a controller serves exactly one frontend app — no cross-module calls. */
|
|
8
|
+
export interface ControllerSchema extends SchemaBase {
|
|
9
|
+
type: 'controller';
|
|
10
|
+
/** The frontend app this controller serves (shared instance from project.config). */
|
|
11
|
+
app: FrontAppSchema;
|
|
12
|
+
/** RPC methods exposed by this controller. */
|
|
13
|
+
methods: ControllerMethodSchema[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function defineController(options: {
|
|
17
|
+
name: string;
|
|
18
|
+
app: FrontAppSchema;
|
|
19
|
+
/** Method declarations: type/schema are injected by this builder. */
|
|
20
|
+
methods: Array<Omit<ControllerMethodSchema, 'type' | 'schema'>>;
|
|
21
|
+
description?: string;
|
|
22
|
+
}): ControllerSchema {
|
|
23
|
+
const schema: ControllerSchema = {
|
|
24
|
+
type: 'controller',
|
|
25
|
+
name: options.name,
|
|
26
|
+
description: options.description,
|
|
27
|
+
app: options.app,
|
|
28
|
+
methods: [],
|
|
29
|
+
};
|
|
30
|
+
schema.methods = options.methods.map((method) => ({ type: 'method', schema, ...method }));
|
|
31
|
+
return schema;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** An RPC method exposed by a controller. */
|
|
35
|
+
export interface ControllerMethodSchema extends SchemaBase {
|
|
36
|
+
type: 'method';
|
|
37
|
+
schema: ControllerSchema;
|
|
38
|
+
/** Shared API signature — same instance the page-side provider references. */
|
|
39
|
+
signature: EndpointSchema;
|
|
40
|
+
}
|
package/src/convert.ts
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
|
-
import type { SchemaBase } from './dsl.js';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import type { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { DtoMessage } from './dto.js';
|
|
3
|
+
import type { TableSchema } from './db.js';
|
|
4
|
+
import type { ThirdMethodSchema } from './third-service.js';
|
|
5
|
+
import type { FrontAppSchema } from './project.js';
|
|
6
|
+
|
|
7
|
+
// Schema-collection integration: multiple source collections combine into
|
|
8
|
+
// one target collection (e.g. two entity tables into one dto, or entity
|
|
9
|
+
// columns plus a dto into one third-party wire message).
|
|
10
|
+
|
|
11
|
+
/** A source/target collection of a convert — dto, entity or third-party message. */
|
|
12
|
+
export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema;
|
|
13
|
+
|
|
14
|
+
/** Declares a multi-source → single-target schema integration. */
|
|
15
|
+
export interface ConvertSchema extends SchemaBase {
|
|
16
|
+
type: 'convert';
|
|
17
|
+
/** The app (module) this convert belongs to — its artifact lands in modules/{app}/convert/. */
|
|
18
|
+
app: FrontAppSchema;
|
|
19
|
+
/** Source schemas — one or more, mixed dimensions. */
|
|
20
|
+
sources: ConvertSourceSchema[];
|
|
21
|
+
/** Target schema — the single integrated collection. */
|
|
22
|
+
target: ConvertSourceSchema;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function defineConvert(options: {
|
|
26
|
+
name: string;
|
|
27
|
+
app: FrontAppSchema;
|
|
28
|
+
sources: ConvertSourceSchema[];
|
|
29
|
+
target: ConvertSourceSchema;
|
|
30
|
+
description?: string;
|
|
31
|
+
}): ConvertSchema {
|
|
32
|
+
if (options.sources.length === 0) {
|
|
33
|
+
throw new Error(`convert '${options.name}': sources must not be empty`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
type: 'convert',
|
|
37
|
+
name: options.name,
|
|
38
|
+
description: options.description,
|
|
39
|
+
app: options.app,
|
|
40
|
+
sources: options.sources,
|
|
41
|
+
target: options.target,
|
|
42
|
+
};
|
|
16
43
|
}
|
package/src/curd.ts
CHANGED
|
@@ -1,94 +1,99 @@
|
|
|
1
|
-
import { SchemaBase, Field, Operator } from './dsl.js';
|
|
2
|
-
import { TableSchema } from './db.js';
|
|
3
|
-
import { FrontAppSchema } from './project.js';
|
|
4
|
-
import { ActionSchema } from './action.js';
|
|
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
|
-
columns
|
|
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
|
-
|
|
1
|
+
import { SchemaBase, Field, Operator } from './dsl.js';
|
|
2
|
+
import { TableSchema } from './db.js';
|
|
3
|
+
import { FrontAppSchema } from './project.js';
|
|
4
|
+
import { ActionSchema } from './action.js';
|
|
5
|
+
import { toKebabCase } from '@pylonts/core';
|
|
6
|
+
|
|
7
|
+
// Admin-only CRUD page standard: binds one entity table to a frontend admin
|
|
8
|
+
// app, describing everything needed to generate the list page plus optional
|
|
9
|
+
// add/update/detail pages. Field-level columns are plain Field instances
|
|
10
|
+
// (table fields, cross-table refs allowed) — DTOs are derived by the generator,
|
|
11
|
+
// the schema itself never references DtoMessage.
|
|
12
|
+
|
|
13
|
+
/** Mode of an action page: modal dialog or standalone route. */
|
|
14
|
+
export type ActionPageMode = 'modal' | 'route';
|
|
15
|
+
|
|
16
|
+
/** One CRUD action page (add / update / detail). */
|
|
17
|
+
export interface ActionPage {
|
|
18
|
+
mode: ActionPageMode;
|
|
19
|
+
/** Fields rendered on this page. Required, non-empty — every field the
|
|
20
|
+
* frontend shows must be listed explicitly. */
|
|
21
|
+
columns: Field[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** List page configuration. */
|
|
25
|
+
export interface CurdListConfig {
|
|
26
|
+
/** List columns; required, non-empty. Every field the list shows must be
|
|
27
|
+
* listed explicitly. May include cross-table fields via foreign refs. */
|
|
28
|
+
columns: Field[];
|
|
29
|
+
/** Fuzzy keyword search on this table's columns. */
|
|
30
|
+
keyword?: { columns: Field[] };
|
|
31
|
+
/** Default sort. Required — column and direction are both mandatory. */
|
|
32
|
+
orderBy: { column: Field; direction: 'asc' | 'desc' };
|
|
33
|
+
/** Search condition fields; op defaults to 'eq'. */
|
|
34
|
+
searchFields?: { field: Field; op?: Operator }[];
|
|
35
|
+
/** Column header text overrides: Field.name → header text. */
|
|
36
|
+
columnTitles?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Admin-only CRUD page standard: binds one entity table to a frontend
|
|
40
|
+
* admin app. Drives generation of the list page plus optional
|
|
41
|
+
* add/update/detail pages. */
|
|
42
|
+
export interface CurdSchema extends SchemaBase {
|
|
43
|
+
/** The admin frontend app this CRUD belongs to (shared instance, type must be 'admin'). */
|
|
44
|
+
app: FrontAppSchema;
|
|
45
|
+
/** The bound entity table (shared instance). */
|
|
46
|
+
table: TableSchema;
|
|
47
|
+
/** List page Chinese title. */
|
|
48
|
+
title: string;
|
|
49
|
+
/** Sidebar menu section (group) this CRUD page belongs to. */
|
|
50
|
+
section: string;
|
|
51
|
+
/** Extra user actions on this page (beyond the standard CRUD). */
|
|
52
|
+
actions?: ActionSchema[];
|
|
53
|
+
/** Add/update/detail action pages. */
|
|
54
|
+
actionPages?: {
|
|
55
|
+
add?: ActionPage;
|
|
56
|
+
update?: ActionPage;
|
|
57
|
+
detail?: ActionPage;
|
|
58
|
+
};
|
|
59
|
+
list: CurdListConfig;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertColumns(curd: CurdSchema, pageName: string, columns: Field[]): void {
|
|
63
|
+
if (columns.length === 0) {
|
|
64
|
+
throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertFieldsOwnTable(curd: CurdSchema, label: string, fields: Field[]): void {
|
|
69
|
+
for (const f of fields) {
|
|
70
|
+
if (f.schema !== curd.table) {
|
|
71
|
+
throw new Error(`curd ${curd.name}: ${label} field ${f.name} does not belong to table ${curd.table.name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Defines an admin CRUD page standard. Runtime-validates admin app binding,
|
|
77
|
+
* non-empty columns and table field ownership (same style as defineTable). */
|
|
78
|
+
export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): CurdSchema {
|
|
79
|
+
const curd: CurdSchema = { name, ...schema };
|
|
80
|
+
if (curd.app.type !== 'admin') {
|
|
81
|
+
throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
|
|
82
|
+
}
|
|
83
|
+
// The name is the admin route path — it must be the kebab-case table name
|
|
84
|
+
// so paths cannot drift from the table they serve.
|
|
85
|
+
const expectedName = toKebabCase(curd.table.name);
|
|
86
|
+
if (name !== expectedName) {
|
|
87
|
+
throw new Error(`curd name must be '${expectedName}' (kebab-case of table '${curd.table.name}'); got '${name}'`);
|
|
88
|
+
}
|
|
89
|
+
if (!curd.section) {
|
|
90
|
+
throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
|
|
91
|
+
}
|
|
92
|
+
assertColumns(curd, 'list', curd.list.columns);
|
|
93
|
+
for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
|
|
94
|
+
if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
|
|
95
|
+
}
|
|
96
|
+
assertFieldsOwnTable(curd, 'keyword', curd.list.keyword?.columns ?? []);
|
|
97
|
+
assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
|
|
98
|
+
return curd;
|
|
94
99
|
}
|