@pylonts/dsl 1.0.3 → 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/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/dto.md +17 -10
- package/package.json +1 -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 +183 -140
- package/src/utils.ts +6 -0
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
|
|
|
@@ -22,7 +22,7 @@ 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 })).
|
|
25
|
+
keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
|
|
26
26
|
...from(order, [order.fields.mer_id]),
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -30,16 +30,23 @@ buildQuery('OrderPageQuery', {
|
|
|
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
|
package/package.json
CHANGED
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;
|