@pylonts/dsl 1.0.5 → 1.1.1
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 +2 -1
- package/dist/asset.d.ts +48 -0
- package/dist/asset.js +31 -0
- package/dist/bases.d.ts +6 -2
- package/dist/bases.js +10 -6
- package/dist/check-inheritance.js +1 -4
- package/dist/curd.d.ts +60 -0
- package/dist/curd.js +31 -0
- package/dist/db-config.d.ts +8 -0
- package/dist/db-config.js +1 -0
- package/dist/db.d.ts +52 -0
- package/dist/db.js +91 -0
- package/dist/dictionary.d.ts +26 -5
- package/dist/dictionary.js +21 -8
- package/dist/dsl.d.ts +5 -49
- package/dist/dsl.js +12 -103
- package/dist/dto.d.ts +14 -9
- package/dist/dto.js +28 -38
- package/dist/enum-driver.d.ts +1 -1
- package/dist/enum-driver.js +1 -4
- package/dist/flow.d.ts +1 -1
- package/dist/flow.js +3 -8
- package/dist/import-base.d.ts +15 -0
- package/dist/import-base.js +1 -0
- package/dist/index.d.ts +21 -17
- package/dist/index.js +21 -33
- package/dist/mermaid-driver.d.ts +2 -2
- package/dist/mermaid-driver.js +2 -6
- package/dist/mock.d.ts +30 -0
- package/dist/mock.js +18 -0
- package/dist/mysql-driver.d.ts +1 -1
- package/dist/mysql-driver.js +20 -10
- package/dist/page-flow.d.ts +2 -2
- package/dist/page-flow.js +2 -6
- package/dist/page.d.ts +6 -6
- package/dist/page.js +3 -8
- package/dist/pattern.js +2 -6
- package/dist/patterns/retry.d.ts +1 -1
- package/dist/patterns/retry.js +2 -6
- package/dist/project.d.ts +18 -13
- package/dist/project.js +41 -6
- package/dist/prototype.d.ts +1 -1
- package/dist/prototype.js +1 -4
- package/dist/typebox-driver.d.ts +3 -3
- package/dist/typebox-driver.js +28 -24
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -4
- package/docs/curd.md +111 -0
- package/docs/dictionary.md +42 -31
- package/docs/driver.md +42 -42
- package/docs/dto.md +66 -66
- package/docs/enum.md +24 -24
- package/docs/project.md +2 -2
- package/docs/table.md +50 -15
- package/package.json +6 -4
- package/src/asset.ts +63 -0
- package/src/bases.ts +12 -2
- package/src/curd.ts +92 -0
- package/src/db-config.ts +8 -0
- package/src/db.ts +142 -0
- package/src/dictionary.ts +45 -19
- package/src/dsl.ts +182 -281
- package/src/dto.ts +247 -234
- package/src/enum-driver.ts +1 -1
- package/src/flow.ts +1 -1
- package/src/import-base.ts +15 -0
- package/src/index.ts +21 -17
- package/src/mermaid-driver.ts +3 -3
- package/src/mock.ts +45 -0
- package/src/mysql-driver.ts +19 -6
- package/src/page-flow.ts +3 -3
- package/src/page.ts +7 -7
- package/src/patterns/retry.ts +1 -1
- package/src/project.ts +90 -54
- package/src/prototype.ts +1 -1
- package/src/typebox-driver.ts +192 -183
- package/src/utils.ts +5 -0
- package/src/check-inheritance.ts +0 -86
package/docs/driver.md
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
# Driver 模式与产物生成
|
|
2
|
-
|
|
3
|
-
DSL 定义元数据,driver 翻译成目标语言产物。产物与定义解耦,同一份定义可生成不同目标:
|
|
4
|
-
|
|
5
|
-
| 定义 | Driver | 产物 | 消费方 |
|
|
6
|
-
|---|---|---|---|
|
|
7
|
-
| `TableSchema` | mysql-driver | `CREATE TABLE` | MySQL |
|
|
8
|
-
| `EnumDef` | enum-driver | `export enum Xxx { … }` + `XXX_LABEL` | 业务代码 |
|
|
9
|
-
| `DtoMessage` | typebox-driver | `Type.Object({…})` + `Static` 推导 | fastify v5 参数校验 |
|
|
10
|
-
|
|
11
|
-
## 生成 SQL
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
import { buildCreateTableSql } from '@pylonts/dsl';
|
|
15
|
-
|
|
16
|
-
buildCreateTableSql(order); // "CREATE TABLE `order` (\n ..."
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
默认不生成外键约束;需要时:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
buildCreateTableSql(order, { generateForeignKeys: true });
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## 生成枚举源码
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
import { renderEnum } from '@pylonts/dsl';
|
|
29
|
-
|
|
30
|
-
renderEnum(AcquiringType); // TS enum 源码
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## 生成 TypeBox 源码
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import { renderDtoMessage } from '@pylonts/dsl';
|
|
37
|
-
|
|
38
|
-
renderDtoMessage(orderPageQuery, {
|
|
39
|
-
source: 'dto_schema/order/order.
|
|
40
|
-
resolver: (name) => ({ from: '@mall/enums/user', name }), // 枚举引用解析
|
|
41
|
-
});
|
|
42
|
-
```
|
|
1
|
+
# Driver 模式与产物生成
|
|
2
|
+
|
|
3
|
+
DSL 定义元数据,driver 翻译成目标语言产物。产物与定义解耦,同一份定义可生成不同目标:
|
|
4
|
+
|
|
5
|
+
| 定义 | Driver | 产物 | 消费方 |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| `TableSchema` | mysql-driver | `CREATE TABLE` | MySQL |
|
|
8
|
+
| `EnumDef` | enum-driver | `export enum Xxx { … }` + `XXX_LABEL` | 业务代码 |
|
|
9
|
+
| `DtoMessage` | typebox-driver | `Type.Object({…})` + `Static` 推导 | fastify v5 参数校验 |
|
|
10
|
+
|
|
11
|
+
## 生成 SQL
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { buildCreateTableSql } from '@pylonts/dsl';
|
|
15
|
+
|
|
16
|
+
buildCreateTableSql(order); // "CREATE TABLE `order` (\n ..."
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
默认不生成外键约束;需要时:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
buildCreateTableSql(order, { generateForeignKeys: true });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 生成枚举源码
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { renderEnum } from '@pylonts/dsl';
|
|
29
|
+
|
|
30
|
+
renderEnum(AcquiringType); // TS enum 源码
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 生成 TypeBox 源码
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { renderDtoMessage } from '@pylonts/dsl';
|
|
37
|
+
|
|
38
|
+
renderDtoMessage(orderPageQuery, {
|
|
39
|
+
source: 'dto_schema/order/order.dto.ts',
|
|
40
|
+
resolver: (name) => ({ from: '@mall/enums/user', name }), // 枚举引用解析
|
|
41
|
+
});
|
|
42
|
+
```
|
package/docs/dto.md
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
# 定义 DTO(四种方向)
|
|
2
|
-
|
|
3
|
-
DTO 描述接口出入参。方向决定可选性规则,由各方向工厂设置;**规则统一只在 `field.optional === undefined` 时生效**(作者已显式设置的跳过):
|
|
4
|
-
|
|
5
|
-
| 构建器 | 方向 | 可选性规则 |
|
|
6
|
-
|---|---|---|
|
|
7
|
-
| `buildInput` | input | 按 DB
|
|
8
|
-
| `buildOutput` | output | 不设置(保持字段构建器/作者设置) |
|
|
9
|
-
| `buildQuery` | query | 全部可选 |
|
|
10
|
-
| `buildPk` | pk | 主键字段必填,其他字段可选 |
|
|
11
|
-
|
|
12
|
-
## 从表提取字段
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';
|
|
16
|
-
|
|
17
|
-
// 输入:新增订单
|
|
18
|
-
buildInput('OrderAddRequest', { ...from(order, [order.
|
|
19
|
-
|
|
20
|
-
// 输出:订单行
|
|
21
|
-
buildOutput('OrderRow', from(order, [order.
|
|
22
|
-
|
|
23
|
-
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
24
|
-
buildQuery('OrderPageQuery', {
|
|
25
|
-
keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
|
|
26
|
-
...from(order, [order.
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// 主键:按 id 取详情
|
|
30
|
-
buildPk('OrderDetailRequest', from(order, [order.
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
`from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema` 保持指向表。**DTO 字段名转 camelCase**(`mer_id` → `merId`),与 DB 列名(snake_case)分离。`from()` 本身不做任何可选性推断——推断在各方向工厂。
|
|
34
|
-
|
|
35
|
-
## 独立字段
|
|
36
|
-
|
|
37
|
-
不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`、`default`。
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
dtoField(stringField({ maxLength: 32 })).setOperator('like')
|
|
41
|
-
dtoField(intField()).setDefault(0) // TypeBox default 注解
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## 默认值
|
|
45
|
-
|
|
46
|
-
- `setDefault(v)` 设 DTO 层默认值,渲染为 TypeBox `default:` 注解(`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`)。
|
|
47
|
-
- 枚举默认值必须是该枚举的成员值(value),渲染时解析为成员引用;非成员值直接报错。
|
|
48
|
-
- 未显式设置时,fallback 到字段构建器的 `default`(DB 默认值,string),`from()` 提取的字段自动带出。
|
|
49
|
-
|
|
50
|
-
## 继承基础 schema
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
buildQuery('OrderPageQuery', { ... })
|
|
54
|
-
.include({ from: '@pylonts/core', name: 'PageRequest' }); // 渲染 Type.Intersect([PageRequest, ...])
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## 关键语义
|
|
58
|
-
|
|
59
|
-
- **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
|
|
60
|
-
- **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
|
|
61
|
-
- **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
|
|
62
|
-
|
|
63
|
-
## 文件组织
|
|
64
|
-
|
|
65
|
-
- `dto_schema/{module}/*.dto.ts`:DTO 源文件,一个文件可导出多个 DtoMessage;`module` = `project.config.ts` 的 `apps.name`,另加 `common/` 放跨 app 共享 DTO。
|
|
66
|
-
- `gen-cli dto` 按 module 逐个扫描:`dto_schema/{module}/*.dto.ts` → 生成 `dto/{module}/{Name}.dto.ts`。
|
|
1
|
+
# 定义 DTO(四种方向)
|
|
2
|
+
|
|
3
|
+
DTO 描述接口出入参。方向决定可选性规则,由各方向工厂设置;**规则统一只在 `field.optional === undefined` 时生效**(作者已显式设置的跳过):
|
|
4
|
+
|
|
5
|
+
| 构建器 | 方向 | 可选性规则 |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `buildInput` | input | 按 DB 列规则:主键 → 必填;可空 / 有默认 → 可选;其余 → 必填 |
|
|
8
|
+
| `buildOutput` | output | 不设置(保持字段构建器/作者设置) |
|
|
9
|
+
| `buildQuery` | query | 全部可选 |
|
|
10
|
+
| `buildPk` | pk | 主键字段必填,其他字段可选 |
|
|
11
|
+
|
|
12
|
+
## 从表提取字段
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';
|
|
16
|
+
|
|
17
|
+
// 输入:新增订单
|
|
18
|
+
buildInput('OrderAddRequest', { ...from(order, [order.columns.mer_id, order.columns.amount]) });
|
|
19
|
+
|
|
20
|
+
// 输出:订单行
|
|
21
|
+
buildOutput('OrderRow', from(order, [order.columns.id, order.columns.order_no]));
|
|
22
|
+
|
|
23
|
+
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
24
|
+
buildQuery('OrderPageQuery', {
|
|
25
|
+
keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
|
|
26
|
+
...from(order, [order.columns.mer_id]),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 主键:按 id 取详情
|
|
30
|
+
buildPk('OrderDetailRequest', from(order, [order.columns.id]));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema` 保持指向表。**DTO 字段名转 camelCase**(`mer_id` → `merId`),与 DB 列名(snake_case)分离。`from()` 本身不做任何可选性推断——推断在各方向工厂。
|
|
34
|
+
|
|
35
|
+
## 独立字段
|
|
36
|
+
|
|
37
|
+
不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`、`default`。
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
dtoField(stringField({ maxLength: 32 })).setOperator('like')
|
|
41
|
+
dtoField(intField()).setDefault(0) // TypeBox default 注解
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 默认值
|
|
45
|
+
|
|
46
|
+
- `setDefault(v)` 设 DTO 层默认值,渲染为 TypeBox `default:` 注解(`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`)。
|
|
47
|
+
- 枚举默认值必须是该枚举的成员值(value),渲染时解析为成员引用;非成员值直接报错。
|
|
48
|
+
- 未显式设置时,fallback 到字段构建器的 `default`(DB 默认值,string),`from()` 提取的字段自动带出。
|
|
49
|
+
|
|
50
|
+
## 继承基础 schema
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
buildQuery('OrderPageQuery', { ... })
|
|
54
|
+
.include({ from: '@pylonts/core', name: 'PageRequest' }); // 渲染 Type.Intersect([PageRequest, ...])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 关键语义
|
|
58
|
+
|
|
59
|
+
- **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
|
|
60
|
+
- **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
|
|
61
|
+
- **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
|
|
62
|
+
|
|
63
|
+
## 文件组织
|
|
64
|
+
|
|
65
|
+
- `dto_schema/{module}/*.dto.ts`:DTO 源文件,一个文件可导出多个 DtoMessage;`module` = `project.config.ts` 的 `apps.name`,另加 `common/` 放跨 app 共享 DTO。
|
|
66
|
+
- `gen-cli dto` 按 module 逐个扫描:`dto_schema/{module}/*.dto.ts` → 生成 `dto/{module}/{Name}.dto.ts`。
|
|
67
67
|
- 枚举字段引用 `enums/` 下生成的枚举源文件,DTO 文件通过 `../../enums/{JsName}.enum` 导入。
|
package/docs/enum.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
# 定义枚举(可跨表复用)
|
|
2
|
-
|
|
3
|
-
枚举定义与字段分离:`defineEnum` 产生共享定义(纯值对象),`enumField` 引用它。同一枚举可被多张表 / 多个 DTO 复用,只生成一次。
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
import { defineEnum, enumField } from '@pylonts/dsl';
|
|
7
|
-
|
|
8
|
-
// 共享定义(_common.ts 等公共文件)
|
|
9
|
-
export const AcquiringType = defineEnum('AcquiringType', 'string', [
|
|
10
|
-
{ symbol: 'WECHAT', value: 'wechat', label: '微信' },
|
|
11
|
-
{ symbol: 'UNIONPAY', value: 'unionpay', label: '银联商务' },
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
// 字段引用(每表独立实例)
|
|
15
|
-
buildTable('merchant', {
|
|
16
|
-
buildTable('order', {
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
- 字段实例每表独立(列名、可选性随表),枚举定义全局共享。
|
|
20
|
-
- 枚举由 enum-driver 生成独立文件;typebox-driver 只渲染 `Type.Enum(名称)` + import 引用,不内联。
|
|
21
|
-
|
|
22
|
-
## 生成枚举源码
|
|
23
|
-
|
|
24
|
-
见 [driver.md](./driver.md)。
|
|
1
|
+
# 定义枚举(可跨表复用)
|
|
2
|
+
|
|
3
|
+
枚举定义与字段分离:`defineEnum` 产生共享定义(纯值对象),`enumField` 引用它。同一枚举可被多张表 / 多个 DTO 复用,只生成一次。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { defineEnum, enumField } from '@pylonts/dsl';
|
|
7
|
+
|
|
8
|
+
// 共享定义(_common.ts 等公共文件)
|
|
9
|
+
export const AcquiringType = defineEnum('AcquiringType', 'string', [
|
|
10
|
+
{ symbol: 'WECHAT', value: 'wechat', label: '微信' },
|
|
11
|
+
{ symbol: 'UNIONPAY', value: 'unionpay', label: '银联商务' },
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// 字段引用(每表独立实例)
|
|
15
|
+
buildTable('merchant', { columns: { acquiring_type: enumField({ enum: AcquiringType }) }, ... });
|
|
16
|
+
buildTable('order', { columns: { acquiring_type: enumField({ enum: AcquiringType }) }, ... });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- 字段实例每表独立(列名、可选性随表),枚举定义全局共享。
|
|
20
|
+
- 枚举由 enum-driver 生成独立文件;typebox-driver 只渲染 `Type.Enum(名称)` + import 引用,不内联。
|
|
21
|
+
|
|
22
|
+
## 生成枚举源码
|
|
23
|
+
|
|
24
|
+
见 [driver.md](./driver.md)。
|
package/docs/project.md
CHANGED
|
@@ -18,7 +18,7 @@ export const mall = defineProject('mall', {
|
|
|
18
18
|
});
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
21
|
+
- `FrontAppSchema`:`name` / `description` / `type`(admin | wxmini)/ `dir`(相对仓库根目录的源码目录)。
|
|
22
|
+
- `ProjectApiSchema`:`name` / `description` / `dir` / `apps`(直接引用共享的 FrontAppSchema 实例——一个 app 被多个 API 服务就定义一次、引用多次)/ `contextPath`(API 基础 URL 前缀,如 `/mall`,空串表示无前缀)。
|
|
23
23
|
- **直接对象引用优先**:`api.apps` 与 `project.apps` 指向同一实例,不写字符串。
|
|
24
24
|
- **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
|
package/docs/table.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| `enumField` | enum | string / number | VARCHAR(20) / TINYINT | 引用共享枚举定义,见 [enum.md](./enum.md) |
|
|
17
17
|
| `jsonField` | json | object | JSON | |
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
通用扩展属性(构建器参数):`label`(中文标签)、`description`、`optional`、`readOnly`、`default`。
|
|
20
20
|
|
|
21
21
|
## 定义表
|
|
22
22
|
|
|
@@ -27,8 +27,8 @@ const id = bigintField({ readOnly: true, label: '主键' });
|
|
|
27
27
|
|
|
28
28
|
export const order = defineTable('order', {
|
|
29
29
|
description: '订单',
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
autoIncrement: id,
|
|
31
|
+
columns: {
|
|
32
32
|
id,
|
|
33
33
|
order_no: stringField({ label: '订单号', maxLength: 32, optional: false }),
|
|
34
34
|
amount: decimalField({ precision: 18, scale: 2, label: '金额' }),
|
|
@@ -37,15 +37,40 @@ export const order = defineTable('order', {
|
|
|
37
37
|
});
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
- 字段名从 map key 反写,`
|
|
40
|
+
- 字段名从 map key 反写,`columns` 里的 key 就是列名。
|
|
41
41
|
- 字段实例不可跨表复用(复用同一字段实例会抛错),枚举除外。
|
|
42
42
|
|
|
43
|
+
## 主键生成策略
|
|
44
|
+
|
|
45
|
+
`autoIncrement` 与 `generator` 互斥,二者选一:
|
|
46
|
+
|
|
47
|
+
| 属性 | 含义 | 例子 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `autoIncrement` | 引用自增主键字段,数据库负责生成值(MySQL `AUTO_INCREMENT`)。设了该属性的字段在 DTO 中自动标记为 optional(写入时不需要传) | `autoIncrement: id` |
|
|
50
|
+
| `generator` | 主键由业务侧生成(非数据库自增),告诉下游工具用哪个 ID 生成器 | `generator: 'snowflake'` |
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// 数据库自增主键
|
|
54
|
+
export const t1 = defineTable('t1', {
|
|
55
|
+
autoIncrement: id,
|
|
56
|
+
columns: { id: bigintField({ readOnly: true, label: '主键' }) },
|
|
57
|
+
primaryKey: id,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// 业务生成主键(snowflake)
|
|
61
|
+
export const t2 = defineTable('t2', {
|
|
62
|
+
generator: 'snowflake',
|
|
63
|
+
columns: { id: bigintField({ readOnly: true, label: '主键' }) },
|
|
64
|
+
primaryKey: id,
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
43
68
|
## 索引
|
|
44
69
|
|
|
45
70
|
```ts
|
|
46
71
|
indexes: [
|
|
47
|
-
{ name: 'uk_uuid',
|
|
48
|
-
{
|
|
72
|
+
{ name: 'uk_uuid', columns: c_uuid, unique: true },
|
|
73
|
+
{ columns: [c_enum, c_date] }, // 名字缺省时 = 字段名 join '_'
|
|
49
74
|
],
|
|
50
75
|
```
|
|
51
76
|
|
|
@@ -55,44 +80,54 @@ indexes: [
|
|
|
55
80
|
|
|
56
81
|
```ts
|
|
57
82
|
// schema/_dictionary.ts
|
|
58
|
-
import {
|
|
83
|
+
import { defineEntityPhrase } from '@pylonts/dsl';
|
|
59
84
|
|
|
60
|
-
const bd =
|
|
85
|
+
export const bd = defineEntityPhrase({ name: 'bd', label: 'BD推广员', description: '线下拓展商户的推广人员' });
|
|
61
86
|
```
|
|
62
87
|
|
|
63
88
|
```ts
|
|
64
|
-
|
|
89
|
+
// schema/bd.table.ts
|
|
90
|
+
import { bigintField, defineTable } from '@pylonts/dsl';
|
|
91
|
+
import { bd as bdPhrase } from './_dictionary';
|
|
65
92
|
|
|
66
93
|
const bdId = bigintField({ readOnly: true, label: 'BD ID' });
|
|
67
94
|
|
|
68
95
|
export const bd = defineTable('bd', {
|
|
69
96
|
description: 'BD',
|
|
70
|
-
phrase:
|
|
71
|
-
|
|
97
|
+
phrase: bdPhrase, // 链接词典条目:本表归属的实体
|
|
98
|
+
columns: { id: bdId },
|
|
72
99
|
primaryKey: bdId,
|
|
73
100
|
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// schema/audit.table.ts
|
|
105
|
+
import { bigintField, defineTable } from '@pylonts/dsl';
|
|
106
|
+
import { bd } from './bd.table';
|
|
74
107
|
|
|
75
108
|
const auditBdId = bigintField({ label: 'BD' });
|
|
76
109
|
|
|
77
110
|
export const audit = defineTable('audit', {
|
|
78
111
|
description: '审核',
|
|
79
|
-
|
|
112
|
+
columns: {
|
|
80
113
|
bd_id: auditBdId, // 列名必须 = 短语 + '_' + 被引用字段名
|
|
81
114
|
},
|
|
82
115
|
foreignKeys: {
|
|
83
|
-
|
|
116
|
+
fk_audit_bd: { columns: auditBdId, references: bd.columns.id },
|
|
84
117
|
},
|
|
85
118
|
});
|
|
86
119
|
```
|
|
87
120
|
|
|
88
121
|
**规则(defineTable 时强制检查)**:外键字段名必须等于 `被引用表.phrase.name + "_" + 被引用字段名`。即引用 `bd.id` 的字段必须叫 `bd_id`——`bd` 来自词典(权威短语),`id` 是 `bd` 表主键。
|
|
89
122
|
|
|
90
|
-
**短语口径**:`
|
|
123
|
+
**短语口径**:`defineEntityPhrase`(实体短语)解释的**就是短语本身**——`name` 即短语词干(如 `mer`),不是实体全名。引用 `merchant` 实体的字段用短语 `mer`(`mer_id`),**不用长语**(`merchant_id`)。短语要短(mer / bd / amt 三字母左右),语义由 `label`/`description` 解释。`TableSchema.phrase` 只接受实体短语(`defineEntityPhrase` 产物);业务短语(`defineBusinessPhrase`)用于字段命名后缀校验,见 [field-check.md](../../lint/docs/field-check.md)。
|
|
91
124
|
|
|
92
125
|
- 被引用表未定义 `phrase` → 抛错(检查链要求每个被引用表都有短语)。
|
|
93
126
|
- 命名不匹配 → 抛错并提示期望名,例如:
|
|
94
127
|
`foreign key bad: field must be named bd_id (phrase bd + id), got merchant_id`
|
|
95
|
-
-
|
|
128
|
+
- 关联表不需要 `phrase`。
|
|
129
|
+
|
|
130
|
+
> **外键是逻辑作用**:`foreignKeys` 用于定义期命名强校验与关系表达,**DDL 默认不渲染物理 FOREIGN KEY 约束**(`gen-cli sql init` 不传 `generateForeignKeys`)。数据完整性由 Service/DAO 层保证;如需物理约束,调用 `buildCreateTableSql(schema, { generateForeignKeys: true })`。
|
|
96
131
|
|
|
97
132
|
## 生成 SQL
|
|
98
133
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonts/dsl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
|
|
5
|
-
"type": "
|
|
5
|
+
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc -p tsconfig.build.json",
|
|
15
15
|
"prepublishOnly": "npm run build",
|
|
16
|
+
"test": "vitest run",
|
|
16
17
|
"typecheck": "npx tsc --noEmit"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"author": "",
|
|
25
26
|
"license": "MIT",
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"typescript": "^7.0.2"
|
|
28
|
+
"typescript": "^7.0.2",
|
|
29
|
+
"vitest": "^4.1.10"
|
|
28
30
|
}
|
|
29
|
-
}
|
|
31
|
+
}
|
package/src/asset.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineAsset — registry for reusable project assets (utils, components, flows, pages, hooks).
|
|
3
|
+
*
|
|
4
|
+
* Each asset declares its name, category, import path, tags, and optional usage example.
|
|
5
|
+
* CLI scans all asset declarations, supports query (by tag/category) and generate (import links).
|
|
6
|
+
*
|
|
7
|
+
* // assets/utils.assets.ts
|
|
8
|
+
* import { defineAsset } from '@pylonts/dsl';
|
|
9
|
+
* export const formatAmt = defineAsset({
|
|
10
|
+
* name: 'formatAmt',
|
|
11
|
+
* category: 'util',
|
|
12
|
+
* tags: ['amount', 'format'],
|
|
13
|
+
* import: { name: 'formatAmt', from: '@/utils/amount' },
|
|
14
|
+
* example: 'formatAmt(12345) => "12,345.00"',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // CLI:
|
|
18
|
+
* // gen-cli asset list --tag form → all form-related assets
|
|
19
|
+
* // gen-cli asset import formatAmt → import { formatAmt } from '@/utils/amount';
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type AssetCategory = 'util' | 'component' | 'flow' | 'page' | 'hook';
|
|
23
|
+
|
|
24
|
+
export interface AssetImport {
|
|
25
|
+
/** Named export, e.g. 'formatAmt' */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Module path, e.g. '@/utils/amount' */
|
|
28
|
+
from: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AssetConfig {
|
|
32
|
+
name: string;
|
|
33
|
+
category: AssetCategory;
|
|
34
|
+
tags: string[];
|
|
35
|
+
import: AssetImport;
|
|
36
|
+
description?: string;
|
|
37
|
+
/** One-liner usage example */
|
|
38
|
+
example?: string;
|
|
39
|
+
/** Link to detailed docs */
|
|
40
|
+
see?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AssetDef {
|
|
44
|
+
name: string;
|
|
45
|
+
category: AssetCategory;
|
|
46
|
+
tags: string[];
|
|
47
|
+
import: AssetImport;
|
|
48
|
+
description?: string;
|
|
49
|
+
example?: string;
|
|
50
|
+
see?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function defineAsset(config: AssetConfig): AssetDef {
|
|
54
|
+
return {
|
|
55
|
+
name: config.name,
|
|
56
|
+
category: config.category,
|
|
57
|
+
tags: config.tags,
|
|
58
|
+
import: config.import,
|
|
59
|
+
description: config.description,
|
|
60
|
+
example: config.example,
|
|
61
|
+
see: config.see,
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/bases.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DtoMessage, ImportRef } from './dto';
|
|
1
|
+
import type { DtoMessage, ImportBase, ImportRef } from './dto.js';
|
|
2
2
|
|
|
3
3
|
// Named base-schema references for common protocol DTOs.
|
|
4
4
|
//
|
|
@@ -10,7 +10,7 @@ import type { DtoMessage, ImportRef } from './dto';
|
|
|
10
10
|
// already-resolved reference — no static analysis or name lookup needed.
|
|
11
11
|
|
|
12
12
|
/** Paginated query request base — renders `import { PageRequest } from '@pylonts/core'` + Intersect */
|
|
13
|
-
export const PageRequest:
|
|
13
|
+
export const PageRequest: ImportBase = { from: '@pylonts/core', name: 'PageRequest' };
|
|
14
14
|
|
|
15
15
|
/** Paginated list response base — renders `import { PageResult } from '@pylonts/core'` + `PageResult(<row>)` */
|
|
16
16
|
export const PageResult = (row: DtoMessage): ImportRef => ({
|
|
@@ -18,3 +18,13 @@ export const PageResult = (row: DtoMessage): ImportRef => ({
|
|
|
18
18
|
name: 'PageResult',
|
|
19
19
|
args: [row],
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
/** Paged rows type base without generic args — renders `import { PagedRows } from '@pylonts/core'` */
|
|
23
|
+
export const PagedRows: ImportBase = { from: '@pylonts/core', name: 'PagedRows' };
|
|
24
|
+
|
|
25
|
+
/** Paged rows type base — renders `import { PagedRows } from '@pylonts/core'` + `PagedRows(<row>)` */
|
|
26
|
+
export const PageRows = (row: DtoMessage): ImportRef => ({
|
|
27
|
+
from: '@pylonts/core',
|
|
28
|
+
name: 'PagedRows',
|
|
29
|
+
args: [row],
|
|
30
|
+
});
|
package/src/curd.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { SchemaBase, Field, Operator } from './dsl.js';
|
|
2
|
+
import { TableSchema } from './db.js';
|
|
3
|
+
import { FrontAppSchema } from './project.js';
|
|
4
|
+
import { ActionSchema } from './page.js';
|
|
5
|
+
|
|
6
|
+
// Admin-only CRUD page standard: binds one entity table to a frontend admin
|
|
7
|
+
// app, describing everything needed to generate the list page plus optional
|
|
8
|
+
// add/update/detail pages. Field-level columns are plain Field instances
|
|
9
|
+
// (table fields, cross-table refs allowed) — DTOs are derived by the generator,
|
|
10
|
+
// the schema itself never references DtoMessage.
|
|
11
|
+
|
|
12
|
+
/** Mode of an action page: modal dialog or standalone route. */
|
|
13
|
+
export type ActionPageMode = 'modal' | 'route';
|
|
14
|
+
|
|
15
|
+
/** One CRUD action page (add / update / detail). */
|
|
16
|
+
export interface ActionPage {
|
|
17
|
+
mode: ActionPageMode;
|
|
18
|
+
/** Fields rendered on this page. Required, non-empty — every field the
|
|
19
|
+
* frontend shows must be listed explicitly. */
|
|
20
|
+
columns: Field[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** List page configuration. */
|
|
24
|
+
export interface CurdListConfig {
|
|
25
|
+
/** List columns; required, non-empty. Every field the list shows must be
|
|
26
|
+
* listed explicitly. May include cross-table fields via foreign refs. */
|
|
27
|
+
columns: Field[];
|
|
28
|
+
/** Fuzzy keyword search on this table's columns. */
|
|
29
|
+
keyword?: { columns: Field[] };
|
|
30
|
+
/** Default sort. Required — column and direction are both mandatory. */
|
|
31
|
+
orderBy: { column: Field; direction: 'asc' | 'desc' };
|
|
32
|
+
/** Search condition fields; op defaults to 'eq'. */
|
|
33
|
+
searchFields?: { field: Field; op?: Operator }[];
|
|
34
|
+
/** Column header text overrides: Field.name → header text. */
|
|
35
|
+
columnTitles?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Admin-only CRUD page standard: binds one entity table to a frontend
|
|
39
|
+
* admin app. Drives generation of the list page plus optional
|
|
40
|
+
* add/update/detail pages. */
|
|
41
|
+
export interface CurdSchema extends SchemaBase {
|
|
42
|
+
/** The admin frontend app this CRUD belongs to (shared instance, type must be 'admin'). */
|
|
43
|
+
app: FrontAppSchema;
|
|
44
|
+
/** The bound entity table (shared instance). */
|
|
45
|
+
table: TableSchema;
|
|
46
|
+
/** List page Chinese title. */
|
|
47
|
+
title: string;
|
|
48
|
+
/** Sidebar menu section (group) this CRUD page belongs to. */
|
|
49
|
+
section: string;
|
|
50
|
+
/** Extra user actions on this page (beyond the standard CRUD). */
|
|
51
|
+
actions?: ActionSchema[];
|
|
52
|
+
/** Add/update/detail action pages. */
|
|
53
|
+
actionPages?: {
|
|
54
|
+
add?: ActionPage;
|
|
55
|
+
update?: ActionPage;
|
|
56
|
+
detail?: ActionPage;
|
|
57
|
+
};
|
|
58
|
+
list: CurdListConfig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertColumns(curd: CurdSchema, pageName: string, columns: Field[]): void {
|
|
62
|
+
if (columns.length === 0) {
|
|
63
|
+
throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertFieldsOwnTable(curd: CurdSchema, label: string, fields: Field[]): void {
|
|
68
|
+
for (const f of fields) {
|
|
69
|
+
if (f.schema !== curd.table) {
|
|
70
|
+
throw new Error(`curd ${curd.name}: ${label} field ${f.name} does not belong to table ${curd.table.name}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Defines an admin CRUD page standard. Runtime-validates admin app binding,
|
|
76
|
+
* non-empty columns and table field ownership (same style as defineTable). */
|
|
77
|
+
export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): CurdSchema {
|
|
78
|
+
const curd: CurdSchema = { name, ...schema };
|
|
79
|
+
if (curd.app.type !== 'admin') {
|
|
80
|
+
throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
|
|
81
|
+
}
|
|
82
|
+
if (!curd.section) {
|
|
83
|
+
throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
|
|
84
|
+
}
|
|
85
|
+
assertColumns(curd, 'list', curd.list.columns);
|
|
86
|
+
for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
|
|
87
|
+
if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
|
|
88
|
+
}
|
|
89
|
+
assertFieldsOwnTable(curd, 'keyword', curd.list.keyword?.columns ?? []);
|
|
90
|
+
assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
|
|
91
|
+
return curd;
|
|
92
|
+
}
|