@pylonts/dsl 1.0.2 → 1.0.4
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/dictionary.d.ts +6 -1
- package/dist/dictionary.js +6 -1
- package/dist/dsl.d.ts +28 -15
- package/dist/dsl.js +44 -1
- package/dist/dto.d.ts +36 -21
- package/dist/dto.js +92 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mermaid-driver.js +7 -3
- package/dist/typebox-driver.d.ts +6 -0
- package/dist/typebox-driver.js +56 -20
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +8 -0
- package/docs/dictionary.md +14 -3
- package/docs/dto.md +25 -12
- package/docs/table.md +12 -3
- package/package.json +1 -1
- package/src/dictionary.ts +6 -1
- package/src/dsl.ts +50 -19
- package/src/dto.ts +93 -48
- package/src/index.ts +1 -0
- package/src/mermaid-driver.ts +6 -3
- package/src/typebox-driver.ts +63 -20
- package/src/utils.ts +6 -0
package/dist/utils.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── naming conversions ──
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.toCamelCase = toCamelCase;
|
|
5
|
+
/** snake_case → camelCase: mer_id → merId; names without underscores are unchanged */
|
|
6
|
+
function toCamelCase(name) {
|
|
7
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
8
|
+
}
|
package/docs/dictionary.md
CHANGED
|
@@ -5,16 +5,27 @@
|
|
|
5
5
|
- 是基础知识库,很少变更。
|
|
6
6
|
- **能引用就引用**:魔法字符串只在首次出现时使用,之后一律引用词典条目。
|
|
7
7
|
|
|
8
|
+
## 规范位置:schema/_dictionary.ts
|
|
9
|
+
|
|
10
|
+
**所有短语统一定义在 `schema/_dictionary.ts`**,一个文件一处定义;`*.table.ts` 从该文件 import 短语,禁止在表文件里内联定义短语。
|
|
11
|
+
|
|
12
|
+
## 口径:definePhrase 解释短语,name 即短语
|
|
13
|
+
|
|
14
|
+
`definePhrase` 返回的条目**就是短语本身**,不是"全名 + 缩写"两套——`name` 即短语词干(列名前缀),`label`/`description` 解释语义。**变量名与 name 一致**(小写)。短语要短(mer / bd / amt 三字母左右),**不要用长语**:引用商户实体的字段叫 `mer_id`,不叫 `merchant_id`。
|
|
15
|
+
|
|
8
16
|
## 定义
|
|
9
17
|
|
|
10
18
|
```ts
|
|
19
|
+
// schema/_dictionary.ts
|
|
11
20
|
import { definePhrase } from '@pylonts/dsl';
|
|
12
21
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
22
|
+
const bd = definePhrase({ name: 'bd', label: 'BD推广员', description: '线下拓展商户、辅助入驻的推广人员' });
|
|
23
|
+
const amt = definePhrase({ name: 'amt', label: '金额', description: '交易金额,单位分' });
|
|
24
|
+
const mer = definePhrase({ name: 'mer', label: '商户', description: '入驻平台的商户' });
|
|
15
25
|
```
|
|
16
26
|
|
|
17
27
|
## 使用
|
|
18
28
|
|
|
19
29
|
- **表链接实体**:`TableSchema.phrase` 引用实体条目,声明本表归属哪个实体(见 [table.md](./table.md) 的外键检查链)。关联表等多实体场景不需要。
|
|
20
|
-
- 业务短语供字段命名/文档使用,跨团队对齐。
|
|
30
|
+
- 业务短语供字段命名/文档使用,跨团队对齐。
|
|
31
|
+
- 未收录短语的实体保留全名作词干(不臆造缩写),评审时再裁决收录。
|
package/docs/dto.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# 定义 DTO(四种方向)
|
|
2
2
|
|
|
3
|
-
DTO
|
|
3
|
+
DTO 描述接口出入参。方向决定可选性规则,由各方向工厂设置;**规则统一只在 `field.optional === undefined` 时生效**(作者已显式设置的跳过):
|
|
4
4
|
|
|
5
|
-
| 构建器 | 方向 |
|
|
5
|
+
| 构建器 | 方向 | 可选性规则 |
|
|
6
6
|
|---|---|---|
|
|
7
|
-
| `buildInput` | input |
|
|
8
|
-
| `buildOutput` | output |
|
|
9
|
-
| `buildQuery` | query |
|
|
10
|
-
| `buildPk` | pk |
|
|
7
|
+
| `buildInput` | input | 按 DB 列规则:可空 / 有默认 / 自增列 → 可选;NOT NULL 无默认 → 必填 |
|
|
8
|
+
| `buildOutput` | output | 不设置(保持字段构建器/作者设置) |
|
|
9
|
+
| `buildQuery` | query | 全部可选 |
|
|
10
|
+
| `buildPk` | pk | 主键字段必填,其他字段可选 |
|
|
11
11
|
|
|
12
12
|
## 从表提取字段
|
|
13
13
|
|
|
@@ -15,31 +15,38 @@ DTO 描述接口出入参。方向决定语义与可选性规则:
|
|
|
15
15
|
import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';
|
|
16
16
|
|
|
17
17
|
// 输入:新增订单
|
|
18
|
-
buildInput('OrderAddRequest', { ...from(order, [order.fields.
|
|
18
|
+
buildInput('OrderAddRequest', { ...from(order, [order.fields.mer_id, order.fields.amount]) });
|
|
19
19
|
|
|
20
20
|
// 输出:订单行
|
|
21
21
|
buildOutput('OrderRow', from(order, [order.fields.id, order.fields.order_no]));
|
|
22
22
|
|
|
23
23
|
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
24
24
|
buildQuery('OrderPageQuery', {
|
|
25
|
-
keyword: dtoField(stringField({ maxLength: 32 })).
|
|
26
|
-
...from(order, [order.fields.
|
|
25
|
+
keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
|
|
26
|
+
...from(order, [order.fields.mer_id]),
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
// 主键:按 id 取详情
|
|
30
30
|
buildPk('OrderDetailRequest', from(order, [order.fields.id]));
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema`
|
|
33
|
+
`from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema` 保持指向表。**DTO 字段名转 camelCase**(`mer_id` → `merId`),与 DB 列名(snake_case)分离。`from()` 本身不做任何可选性推断——推断在各方向工厂。
|
|
34
34
|
|
|
35
35
|
## 独立字段
|
|
36
36
|
|
|
37
|
-
不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`。
|
|
37
|
+
不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`、`default`。
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
dtoField(stringField({ maxLength: 32 })).
|
|
40
|
+
dtoField(stringField({ maxLength: 32 })).setOperator('like')
|
|
41
|
+
dtoField(intField()).setDefault(0) // TypeBox default 注解
|
|
41
42
|
```
|
|
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
|
+
|
|
43
50
|
## 继承基础 schema
|
|
44
51
|
|
|
45
52
|
```ts
|
|
@@ -52,3 +59,9 @@ buildQuery('OrderPageQuery', { ... })
|
|
|
52
59
|
- **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
|
|
53
60
|
- **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
|
|
54
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
|
+
- 枚举字段引用 `enums/` 下生成的枚举源文件,DTO 文件通过 `../../enums/{JsName}.enum` 导入。
|
package/docs/table.md
CHANGED
|
@@ -51,16 +51,23 @@ indexes: [
|
|
|
51
51
|
|
|
52
52
|
## 外键与短语检查链
|
|
53
53
|
|
|
54
|
+
短语统一定义在 `schema/_dictionary.ts`(见 [dictionary.md](./dictionary.md)),表文件从那里 import:
|
|
55
|
+
|
|
54
56
|
```ts
|
|
57
|
+
// schema/_dictionary.ts
|
|
55
58
|
import { definePhrase } from '@pylonts/dsl';
|
|
56
59
|
|
|
57
|
-
const
|
|
60
|
+
const bd = definePhrase({ name: 'bd', label: 'BD推广员', description: '线下拓展商户的推广人员' });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { bd } from './_dictionary';
|
|
58
65
|
|
|
59
66
|
const bdId = bigintField({ readOnly: true, label: 'BD ID' });
|
|
60
67
|
|
|
61
68
|
export const bd = defineTable('bd', {
|
|
62
69
|
description: 'BD',
|
|
63
|
-
phrase:
|
|
70
|
+
phrase: bd, // 链接词典条目:本表归属的实体
|
|
64
71
|
fields: { id: bdId },
|
|
65
72
|
primaryKey: bdId,
|
|
66
73
|
});
|
|
@@ -70,7 +77,7 @@ const auditBdId = bigintField({ label: 'BD' });
|
|
|
70
77
|
export const audit = defineTable('audit', {
|
|
71
78
|
description: '审核',
|
|
72
79
|
fields: {
|
|
73
|
-
bd_id: auditBdId, // 列名必须 =
|
|
80
|
+
bd_id: auditBdId, // 列名必须 = 短语 + '_' + 被引用字段名
|
|
74
81
|
},
|
|
75
82
|
foreignKeys: {
|
|
76
83
|
bd_bd_id: { fields: auditBdId, references: bdId },
|
|
@@ -80,6 +87,8 @@ export const audit = defineTable('audit', {
|
|
|
80
87
|
|
|
81
88
|
**规则(defineTable 时强制检查)**:外键字段名必须等于 `被引用表.phrase.name + "_" + 被引用字段名`。即引用 `bd.id` 的字段必须叫 `bd_id`——`bd` 来自词典(权威短语),`id` 是 `bd` 表主键。
|
|
82
89
|
|
|
90
|
+
**短语口径**:`definePhrase` 解释的**就是短语本身**——`name` 即短语词干(如 `mer`),不是实体全名。引用 `merchant` 实体的字段用短语 `mer`(`mer_id`),**不用长语**(`merchant_id`)。短语要短(mer / bd / amt 三字母左右),语义由 `label`/`description` 解释。
|
|
91
|
+
|
|
83
92
|
- 被引用表未定义 `phrase` → 抛错(检查链要求每个被引用表都有短语)。
|
|
84
93
|
- 命名不匹配 → 抛错并提示期望名,例如:
|
|
85
94
|
`foreign key bad: field must be named bd_id (phrase bd + id), got merchant_id`
|
package/package.json
CHANGED
package/src/dictionary.ts
CHANGED
|
@@ -9,7 +9,12 @@ export interface DictionaryEntry extends SchemaBase {
|
|
|
9
9
|
label?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Explains a phrase — the returned entry IS the phrase. `name` is the phrase
|
|
14
|
+
* itself (column-name stem, e.g. mer / bd / amt), not a full entity name;
|
|
15
|
+
* label/description explain what it means. Convention: short phrase, not long
|
|
16
|
+
* name.
|
|
17
|
+
*/
|
|
13
18
|
export function definePhrase(extra: DictionaryEntry): DictionaryEntry {
|
|
14
19
|
return extra;
|
|
15
20
|
}
|
package/src/dsl.ts
CHANGED
|
@@ -8,17 +8,24 @@ export interface SchemaBase {
|
|
|
8
8
|
description?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** Field collection schemas (DB table vs DTO message); `type` is the discriminator */
|
|
12
|
+
export interface CollectionSchemaBase extends SchemaBase {
|
|
13
|
+
type: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export interface BaseField {
|
|
12
17
|
name: string;
|
|
13
18
|
/** 显示名称(中文标签) */
|
|
14
19
|
label?: string;
|
|
15
20
|
/** 字段描述 */
|
|
16
21
|
description?: string;
|
|
22
|
+
/** 业务语义码(如 'merchant_name'),驱动 mock 生成等下游消费 */
|
|
23
|
+
semantic?: string;
|
|
17
24
|
optional?: boolean;
|
|
18
25
|
readOnly?: boolean;
|
|
19
26
|
default?: string;
|
|
20
27
|
/** 所属 schema(db 或 dto) */
|
|
21
|
-
schema?:
|
|
28
|
+
schema?: CollectionSchemaBase;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
interface StringField extends BaseField {
|
|
@@ -132,7 +139,24 @@ export type ForeignKey = {
|
|
|
132
139
|
references: Field | Field[];
|
|
133
140
|
};
|
|
134
141
|
|
|
135
|
-
export interface
|
|
142
|
+
export interface TableSchemaOptions {
|
|
143
|
+
description?: string;
|
|
144
|
+
paginated?: boolean;
|
|
145
|
+
actor?: boolean;
|
|
146
|
+
generator?: string;
|
|
147
|
+
autoIncrement?: Field;
|
|
148
|
+
primaryKey?: Field | Field[];
|
|
149
|
+
indexes?: Index[];
|
|
150
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
151
|
+
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
152
|
+
phrase?: DictionaryEntry;
|
|
153
|
+
fields: Record<string, Field>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class TableSchema implements CollectionSchemaBase {
|
|
157
|
+
type = 'table';
|
|
158
|
+
name: string;
|
|
159
|
+
description?: string;
|
|
136
160
|
/** 分页 */
|
|
137
161
|
paginated?: boolean;
|
|
138
162
|
/** 系统操作者(如小程序为 C 端用户,管理端为运营) */
|
|
@@ -148,6 +172,28 @@ export interface TableSchema extends SchemaBase {
|
|
|
148
172
|
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
149
173
|
phrase?: DictionaryEntry;
|
|
150
174
|
fields: Record<string, Field>;
|
|
175
|
+
|
|
176
|
+
constructor(name: string, options: TableSchemaOptions) {
|
|
177
|
+
this.name = name;
|
|
178
|
+
this.description = options.description;
|
|
179
|
+
this.paginated = options.paginated;
|
|
180
|
+
this.actor = options.actor;
|
|
181
|
+
this.generator = options.generator;
|
|
182
|
+
this.autoIncrement = options.autoIncrement;
|
|
183
|
+
this.primaryKey = options.primaryKey;
|
|
184
|
+
this.indexes = options.indexes;
|
|
185
|
+
this.foreignKeys = options.foreignKeys;
|
|
186
|
+
this.phrase = options.phrase;
|
|
187
|
+
this.fields = options.fields;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** True when the field is part of this table's primary key */
|
|
191
|
+
isPk(fieldRef: Field): boolean {
|
|
192
|
+
if (this.primaryKey === undefined) return false;
|
|
193
|
+
return Array.isArray(this.primaryKey)
|
|
194
|
+
? this.primaryKey.includes(fieldRef)
|
|
195
|
+
: this.primaryKey === fieldRef;
|
|
196
|
+
}
|
|
151
197
|
}
|
|
152
198
|
|
|
153
199
|
// Field builders: type and jsType are fixed, pass extra properties only.
|
|
@@ -200,23 +246,8 @@ export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): E
|
|
|
200
246
|
return { name: '', type: 'enum', jsType, ...extra };
|
|
201
247
|
}
|
|
202
248
|
|
|
203
|
-
export function defineTable(
|
|
204
|
-
name
|
|
205
|
-
schema: {
|
|
206
|
-
description?: string;
|
|
207
|
-
paginated?: boolean;
|
|
208
|
-
actor?: boolean;
|
|
209
|
-
generator?: string;
|
|
210
|
-
autoIncrement?: Field;
|
|
211
|
-
primaryKey?: Field | Field[];
|
|
212
|
-
indexes?: Index[];
|
|
213
|
-
foreignKeys?: Record<string, ForeignKey>;
|
|
214
|
-
/** 引用的实体短语(词典条目) */
|
|
215
|
-
phrase?: DictionaryEntry;
|
|
216
|
-
fields: Record<string, Field>;
|
|
217
|
-
},
|
|
218
|
-
): TableSchema {
|
|
219
|
-
const table: TableSchema = { name, ...schema };
|
|
249
|
+
export function defineTable(name: string, schema: TableSchemaOptions): TableSchema {
|
|
250
|
+
const table = new TableSchema(name, schema);
|
|
220
251
|
for (const key of Object.keys(table.fields)) {
|
|
221
252
|
const field = table.fields[key];
|
|
222
253
|
if (field.schema && field.schema !== table) {
|
package/src/dto.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { BaseField, Field, SchemaBase, TableSchema } from './dsl';
|
|
1
|
+
import { BaseField, CollectionSchemaBase, Field, SchemaBase, TableSchema } from './dsl';
|
|
2
|
+
import { toCamelCase } from './utils';
|
|
2
3
|
|
|
3
4
|
// Interface (DTO) field definitions.
|
|
4
5
|
// Naming convention: all DTO types and builders use the Dto prefix.
|
|
@@ -16,18 +17,15 @@ export interface ImportRef {
|
|
|
16
17
|
from: string;
|
|
17
18
|
/** Named export, e.g. 'PageRequest' */
|
|
18
19
|
name: string;
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
|
|
22
|
+
* Two forms, both local DTOs:
|
|
23
|
+
* string — the DTO export name
|
|
24
|
+
* DtoMessage — the DTO instance itself; the driver resolves it to its name
|
|
25
|
+
*/
|
|
26
|
+
args?: (string | DtoMessage)[];
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
export type DtoExtras = {
|
|
24
|
-
pattern?: string;
|
|
25
|
-
/** 联合判断是否可选,定义后优先级高于 field.optional */
|
|
26
|
-
optional?: boolean;
|
|
27
|
-
/** 查询比较操作符(query 方向字段) */
|
|
28
|
-
operator?: Operator;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
29
|
export type DtoArrayFieldDef = BaseField & {
|
|
32
30
|
type: 'array';
|
|
33
31
|
jsType: 'array';
|
|
@@ -40,23 +38,24 @@ export type DtoObjectFieldDef = BaseField & {
|
|
|
40
38
|
properties: Record<string, DtoField>;
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
export class DtoField {
|
|
41
|
+
export class DtoField implements SchemaBase {
|
|
44
42
|
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
45
43
|
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
46
44
|
name: string;
|
|
45
|
+
/** 字段描述 */
|
|
46
|
+
description?: string;
|
|
47
47
|
/** 所属 DTO 容器(buildMessage 反写) */
|
|
48
48
|
schema?: DtoMessage;
|
|
49
49
|
field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
|
|
50
50
|
pattern?: string;
|
|
51
51
|
optional?: boolean;
|
|
52
52
|
operator?: Operator;
|
|
53
|
+
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
54
|
+
default?: unknown;
|
|
53
55
|
|
|
54
|
-
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef
|
|
56
|
+
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
|
|
55
57
|
this.name = '';
|
|
56
58
|
this.field = field;
|
|
57
|
-
this.pattern = extra.pattern;
|
|
58
|
-
this.optional = extra.optional;
|
|
59
|
-
this.operator = extra.operator;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
setPattern(value: string): this {
|
|
@@ -64,15 +63,29 @@ export class DtoField {
|
|
|
64
63
|
return this;
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
setDescription(value: string): this {
|
|
67
|
+
this.description = value;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getDescription(): string | undefined {
|
|
72
|
+
return this.description;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
setOptional(value: boolean): this {
|
|
68
76
|
this.optional = value;
|
|
69
77
|
return this;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
/** Set a default value — emitted as a TypeBox schema default annotation */
|
|
81
|
+
setDefault(value: unknown): this {
|
|
82
|
+
this.default = value;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
73
|
-
|
|
87
|
+
setOperator(value: Operator): this {
|
|
74
88
|
this.operator = value;
|
|
75
|
-
this.optional = true;
|
|
76
89
|
return this;
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -99,46 +112,55 @@ export class DtoObjectField extends DtoField {
|
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
102
|
-
export
|
|
115
|
+
export enum DtoDirection {
|
|
116
|
+
Input = 'input',
|
|
117
|
+
Output = 'output',
|
|
118
|
+
Query = 'query',
|
|
119
|
+
Pk = 'pk',
|
|
120
|
+
}
|
|
103
121
|
|
|
104
|
-
export
|
|
122
|
+
export class DtoMessage implements CollectionSchemaBase {
|
|
123
|
+
type = 'dto';
|
|
124
|
+
name: string;
|
|
125
|
+
description?: string;
|
|
105
126
|
/** 方向:输入或输出 */
|
|
106
127
|
direction: DtoDirection;
|
|
107
128
|
fields: Record<string, DtoField>;
|
|
108
129
|
/** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
|
|
109
|
-
bases
|
|
130
|
+
bases: ImportRef[] = [];
|
|
131
|
+
|
|
132
|
+
constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
|
|
133
|
+
this.name = name;
|
|
134
|
+
this.direction = direction;
|
|
135
|
+
this.fields = fields;
|
|
136
|
+
this.description = description;
|
|
137
|
+
}
|
|
138
|
+
|
|
110
139
|
/** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
|
|
111
|
-
include(...refs: ImportRef[]):
|
|
140
|
+
include(...refs: ImportRef[]): this {
|
|
141
|
+
this.bases.push(...refs);
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
export function dtoField(field: Field
|
|
115
|
-
return new DtoField(field
|
|
146
|
+
export function dtoField(field: Field): DtoField {
|
|
147
|
+
return new DtoField(field);
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
export function dtoArrayField(def: { items: DtoField } & Omit<BaseField, 'name'
|
|
119
|
-
|
|
150
|
+
export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
|
|
151
|
+
// Reuse an existing DTO as the array element: expand its fields into an object.
|
|
152
|
+
const items = def.items instanceof DtoMessage
|
|
153
|
+
? new DtoObjectField({ name: '', type: 'object', jsType: 'object', properties: def.items.fields })
|
|
154
|
+
: def.items;
|
|
155
|
+
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def, items });
|
|
120
156
|
}
|
|
121
157
|
|
|
122
|
-
export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'
|
|
123
|
-
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def }
|
|
158
|
+
export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
|
|
159
|
+
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
|
|
124
160
|
}
|
|
125
161
|
|
|
126
162
|
function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
127
|
-
const message
|
|
128
|
-
name,
|
|
129
|
-
direction,
|
|
130
|
-
description,
|
|
131
|
-
fields,
|
|
132
|
-
bases: [],
|
|
133
|
-
include(...refs: ImportRef[]): DtoMessage {
|
|
134
|
-
this.bases!.push(...refs);
|
|
135
|
-
return this;
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
if (direction === 'query') {
|
|
139
|
-
// Rule B: query/search fields are always optional.
|
|
140
|
-
for (const field of Object.values(message.fields)) field.optional = true;
|
|
141
|
-
}
|
|
163
|
+
const message = new DtoMessage(name, direction, fields, description);
|
|
142
164
|
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
143
165
|
// are created per DTO, never shared).
|
|
144
166
|
for (const key of Object.keys(message.fields)) {
|
|
@@ -157,19 +179,40 @@ function buildMessage(name: string, direction: DtoDirection, fields: Record<stri
|
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
160
|
-
|
|
182
|
+
const message = buildMessage(name, DtoDirection.Input, fields, description);
|
|
183
|
+
// Rule A — set optionality from the DB column rule (skips fields the author
|
|
184
|
+
// already set): nullable / default / auto-increment → optional, else required.
|
|
185
|
+
for (const field of Object.values(message.fields)) {
|
|
186
|
+
if (field.optional !== undefined) continue;
|
|
187
|
+
const f = field.field as Field;
|
|
188
|
+
if (f.schema?.type !== 'table') continue;
|
|
189
|
+
const table = f.schema as TableSchema;
|
|
190
|
+
field.optional = f.optional !== false || f.default !== undefined || table.autoIncrement === f;
|
|
191
|
+
}
|
|
192
|
+
return message;
|
|
161
193
|
}
|
|
162
194
|
|
|
163
195
|
export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
164
|
-
return buildMessage(name,
|
|
196
|
+
return buildMessage(name, DtoDirection.Output, fields, description);
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
168
|
-
|
|
200
|
+
const message = buildMessage(name, DtoDirection.Query, fields, description);
|
|
201
|
+
// Rule B: query/search fields are always optional.
|
|
202
|
+
for (const field of Object.values(message.fields)) field.optional = true;
|
|
203
|
+
return message;
|
|
169
204
|
}
|
|
170
205
|
|
|
171
206
|
export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
172
|
-
|
|
207
|
+
const message = buildMessage(name, DtoDirection.Pk, fields, description);
|
|
208
|
+
// Rule P: PK locator fields are required, other fields are optional.
|
|
209
|
+
for (const field of Object.values(message.fields)) {
|
|
210
|
+
if (field.optional !== undefined) continue;
|
|
211
|
+
const f = field.field as Field;
|
|
212
|
+
const table = f.schema as TableSchema | undefined;
|
|
213
|
+
field.optional = table !== undefined && table.isPk(f) ? false : true;
|
|
214
|
+
}
|
|
215
|
+
return message;
|
|
173
216
|
}
|
|
174
217
|
|
|
175
218
|
/** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
|
|
@@ -179,7 +222,9 @@ export function from(table: TableSchema, fields: Field[]): Record<string, DtoFie
|
|
|
179
222
|
if (field.schema !== table) {
|
|
180
223
|
throw new Error(`dto.from(${table.name}): field ${field.name} does not belong to this table`);
|
|
181
224
|
}
|
|
182
|
-
|
|
225
|
+
// DTO field name is camelCase (mer_id → merId); the underlying field.name
|
|
226
|
+
// stays snake_case (DB column).
|
|
227
|
+
out[toCamelCase(field.name)] = dtoField(field);
|
|
183
228
|
}
|
|
184
229
|
return out;
|
|
185
230
|
}
|
package/src/index.ts
CHANGED
package/src/mermaid-driver.ts
CHANGED
|
@@ -53,9 +53,12 @@ export function renderPageFlowMermaid(schema: PageFlow): string {
|
|
|
53
53
|
|
|
54
54
|
const byApp = new Map<string, Page[]>();
|
|
55
55
|
for (const p of schema.pages) {
|
|
56
|
-
const list = byApp.get(p.app.name)
|
|
57
|
-
list
|
|
58
|
-
|
|
56
|
+
const list = byApp.get(p.app.name);
|
|
57
|
+
if (!list) {
|
|
58
|
+
byApp.set(p.app.name, [p]);
|
|
59
|
+
} else {
|
|
60
|
+
list.push(p);
|
|
61
|
+
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
let appIdx = 0;
|