@pylonts/dsl 1.1.14 → 1.1.16
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/typebox-driver.js +95 -29
- package/docs/third-service.md +100 -70
- package/package.json +2 -2
- package/src/typebox-driver.ts +303 -234
package/dist/typebox-driver.js
CHANGED
|
@@ -8,11 +8,21 @@ function renderDefault(v) {
|
|
|
8
8
|
return renderString(v);
|
|
9
9
|
return JSON.stringify(v);
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
/** BaseField-level description: description takes priority over label. */
|
|
12
|
+
function fieldDescription(field) {
|
|
13
|
+
return field.description ?? field.label;
|
|
14
|
+
}
|
|
15
|
+
/** DtoField-level description: own description > referenced field description/label. */
|
|
16
|
+
function dtoFieldDescription(f) {
|
|
17
|
+
return f.description ?? fieldDescription(f.field);
|
|
18
|
+
}
|
|
19
|
+
function renderBasic(field, pattern, defaultValue, resolver, indent = 0, description) {
|
|
12
20
|
if (pattern !== undefined && field.type !== 'string') {
|
|
13
21
|
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
14
22
|
}
|
|
15
23
|
const def = defaultValue !== undefined ? `default: ${renderDefault(defaultValue)}` : undefined;
|
|
24
|
+
const desc = description !== undefined ? `description: ${renderString(description)}` : undefined;
|
|
25
|
+
const withOpts = (base, opts) => opts.length > 0 ? `${base}({ ${opts.join(', ')} })` : `${base}()`;
|
|
16
26
|
switch (field.type) {
|
|
17
27
|
case 'string': {
|
|
18
28
|
const opts = [];
|
|
@@ -24,10 +34,18 @@ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
|
|
|
24
34
|
opts.push(`pattern: ${renderString(pattern)}`);
|
|
25
35
|
if (def !== undefined)
|
|
26
36
|
opts.push(def);
|
|
27
|
-
|
|
37
|
+
if (desc !== undefined)
|
|
38
|
+
opts.push(desc);
|
|
39
|
+
return withOpts('Type.String', opts);
|
|
40
|
+
}
|
|
41
|
+
case 'text': {
|
|
42
|
+
const opts = [];
|
|
43
|
+
if (def !== undefined)
|
|
44
|
+
opts.push(def);
|
|
45
|
+
if (desc !== undefined)
|
|
46
|
+
opts.push(desc);
|
|
47
|
+
return withOpts('Type.String', opts);
|
|
28
48
|
}
|
|
29
|
-
case 'text':
|
|
30
|
-
return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
|
|
31
49
|
case 'integer': {
|
|
32
50
|
const opts = [];
|
|
33
51
|
if (field.min !== undefined)
|
|
@@ -36,21 +54,41 @@ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
|
|
|
36
54
|
opts.push(`maximum: ${field.max}`);
|
|
37
55
|
if (def !== undefined)
|
|
38
56
|
opts.push(def);
|
|
39
|
-
|
|
57
|
+
if (desc !== undefined)
|
|
58
|
+
opts.push(desc);
|
|
59
|
+
return withOpts('Type.Integer', opts);
|
|
40
60
|
}
|
|
41
61
|
case 'bigint':
|
|
42
62
|
case 'decimal':
|
|
43
63
|
case 'rate':
|
|
44
64
|
case 'time':
|
|
45
65
|
case 'date':
|
|
46
|
-
case 'datetime':
|
|
66
|
+
case 'datetime': {
|
|
47
67
|
// Transmitted as string over HTTP: bigint/decimal/rate keep full
|
|
48
68
|
// precision, date/time serialize to string.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
const opts = [];
|
|
70
|
+
if (def !== undefined)
|
|
71
|
+
opts.push(def);
|
|
72
|
+
if (desc !== undefined)
|
|
73
|
+
opts.push(desc);
|
|
74
|
+
return withOpts('Type.String', opts);
|
|
75
|
+
}
|
|
76
|
+
case 'boolean': {
|
|
77
|
+
const opts = [];
|
|
78
|
+
if (def !== undefined)
|
|
79
|
+
opts.push(def);
|
|
80
|
+
if (desc !== undefined)
|
|
81
|
+
opts.push(desc);
|
|
82
|
+
return withOpts('Type.Boolean', opts);
|
|
83
|
+
}
|
|
84
|
+
case 'json': {
|
|
85
|
+
const opts = [];
|
|
86
|
+
if (def !== undefined)
|
|
87
|
+
opts.push(def);
|
|
88
|
+
if (desc !== undefined)
|
|
89
|
+
opts.push(desc);
|
|
90
|
+
return withOpts('Type.Unknown', opts);
|
|
91
|
+
}
|
|
54
92
|
case 'enum': {
|
|
55
93
|
const ref = resolver?.(field.enum.jsName);
|
|
56
94
|
if (!ref)
|
|
@@ -60,18 +98,29 @@ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
|
|
|
60
98
|
if (!member) {
|
|
61
99
|
throw new Error(`enum field ${field.name}: default ${renderDefault(defaultValue)} is not a member of ${field.enum.jsName}`);
|
|
62
100
|
}
|
|
63
|
-
|
|
101
|
+
const opts = [];
|
|
102
|
+
if (desc !== undefined)
|
|
103
|
+
opts.push(desc);
|
|
104
|
+
return `Type.Enum(${ref.name}, { default: ${ref.name}.${member.symbol}${opts.length > 0 ? `, ${opts.join(', ')}` : ''} })`;
|
|
64
105
|
}
|
|
106
|
+
if (desc !== undefined)
|
|
107
|
+
return `Type.Enum(${ref.name}, { ${desc} })`;
|
|
65
108
|
return `Type.Enum(${ref.name})`;
|
|
66
109
|
}
|
|
67
|
-
case 'aggregate':
|
|
110
|
+
case 'aggregate': {
|
|
68
111
|
// Aggregate query outputs: count/sum(int) are numbers, everything else
|
|
69
112
|
// arrives as a precision string.
|
|
70
|
-
|
|
113
|
+
const opts = [];
|
|
114
|
+
if (desc !== undefined)
|
|
115
|
+
opts.push(desc);
|
|
116
|
+
return field.jsType === 'number' ? withOpts('Type.Number', opts) : withOpts('Type.String', opts);
|
|
117
|
+
}
|
|
71
118
|
case 'array':
|
|
72
|
-
return
|
|
119
|
+
return desc !== undefined
|
|
120
|
+
? `Type.Array(${renderFieldValue(field.items, indent, resolver)}, { ${desc} })`
|
|
121
|
+
: `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
|
|
73
122
|
case 'object':
|
|
74
|
-
return renderFieldObject(field.properties, indent + 1, resolver);
|
|
123
|
+
return renderFieldObject(field.properties, indent + 1, resolver, description);
|
|
75
124
|
default:
|
|
76
125
|
// Field union is exhaustive; this branch is unreachable at runtime.
|
|
77
126
|
throw new Error(`unsupported field type: ${String(field.type)}`);
|
|
@@ -79,22 +128,28 @@ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
|
|
|
79
128
|
}
|
|
80
129
|
/** Render a plain Field value (wire-format nested fields), wrapping optional. */
|
|
81
130
|
function renderFieldValue(field, indent, resolver) {
|
|
82
|
-
const base = renderBasic(field, undefined, undefined, resolver, indent);
|
|
131
|
+
const base = renderBasic(field, undefined, undefined, resolver, indent, fieldDescription(field));
|
|
83
132
|
return field.optional ? `Type.Optional(${base})` : base;
|
|
84
133
|
}
|
|
85
134
|
/** Render a plain Field object (wire-format nested object). */
|
|
86
|
-
function renderFieldObject(properties, indent, resolver) {
|
|
135
|
+
function renderFieldObject(properties, indent, resolver, description) {
|
|
87
136
|
const pad = ' '.repeat(indent);
|
|
88
137
|
const entries = Object.entries(properties).map(([name, f]) => `${pad}${name}: ${renderFieldValue(f, indent, resolver)}`);
|
|
89
|
-
|
|
138
|
+
const obj = `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
139
|
+
return description !== undefined
|
|
140
|
+
? `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}}, { description: ${renderString(description)} })`
|
|
141
|
+
: obj;
|
|
90
142
|
}
|
|
91
|
-
function renderObject(fields, indent, resolver) {
|
|
143
|
+
function renderObject(fields, indent, resolver, description) {
|
|
92
144
|
const pad = ' '.repeat(indent);
|
|
93
145
|
const entries = Object.entries(fields).map(([name, f]) => {
|
|
94
146
|
const rendered = isDtoField(f) ? renderField(f, indent, resolver) : renderFieldValue(f, indent, resolver);
|
|
95
147
|
return `${pad}${name}: ${rendered}`;
|
|
96
148
|
});
|
|
97
|
-
|
|
149
|
+
const obj = `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
150
|
+
return description !== undefined
|
|
151
|
+
? `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}}, { description: ${renderString(description)} })`
|
|
152
|
+
: obj;
|
|
98
153
|
}
|
|
99
154
|
function renderField(f, indent, resolver) {
|
|
100
155
|
const base = renderValue(f, indent, resolver);
|
|
@@ -103,20 +158,31 @@ function renderField(f, indent, resolver) {
|
|
|
103
158
|
function renderValue(f, indent, resolver) {
|
|
104
159
|
if (f.field.type === 'array') {
|
|
105
160
|
const items = f.field.items;
|
|
161
|
+
const desc = dtoFieldDescription(f);
|
|
106
162
|
// Referenced DTO element — render by name (same-file export), not expanded.
|
|
107
|
-
if (isDtoMessage(items))
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
163
|
+
if (isDtoMessage(items)) {
|
|
164
|
+
return desc !== undefined
|
|
165
|
+
? `Type.Array(${items.name}, { description: ${renderString(desc)} })`
|
|
166
|
+
: `Type.Array(${items.name})`;
|
|
167
|
+
}
|
|
168
|
+
if (isDtoField(items)) {
|
|
169
|
+
const rendered = `Type.Array(${renderField(items, indent + 1, resolver)})`;
|
|
170
|
+
return desc !== undefined
|
|
171
|
+
? `Type.Array(${renderField(items, indent + 1, resolver)}, { description: ${renderString(desc)} })`
|
|
172
|
+
: rendered;
|
|
173
|
+
}
|
|
174
|
+
const rendered = `Type.Array(${renderFieldValue(items, indent, resolver)})`;
|
|
175
|
+
return desc !== undefined
|
|
176
|
+
? `Type.Array(${renderFieldValue(items, indent, resolver)}, { description: ${renderString(desc)} })`
|
|
177
|
+
: rendered;
|
|
112
178
|
}
|
|
113
179
|
if (f.field.type === 'object') {
|
|
114
|
-
return renderObject(f.field.properties, indent + 1, resolver);
|
|
180
|
+
return renderObject(f.field.properties, indent + 1, resolver, dtoFieldDescription(f));
|
|
115
181
|
}
|
|
116
182
|
// DtoField only wraps a database Field; array/object defs live in the subclasses.
|
|
117
183
|
// Only DTO-level defaults (setDefault) are emitted as TypeBox default
|
|
118
184
|
// annotations; DB field defaults are not carried into the API contract.
|
|
119
|
-
return renderBasic(f.field, f.pattern, f.default, resolver, indent);
|
|
185
|
+
return renderBasic(f.field, f.pattern, f.default, resolver, indent, dtoFieldDescription(f));
|
|
120
186
|
}
|
|
121
187
|
function collectEnumImports(f, resolver, out) {
|
|
122
188
|
if (f.field.type === 'array') {
|
|
@@ -163,7 +229,7 @@ export function collectDtoImports(schema, resolver, out) {
|
|
|
163
229
|
}
|
|
164
230
|
/** Render one DTO export (const + type) — no file header, for file-level generation. */
|
|
165
231
|
export function renderDtoExport(schema, resolver) {
|
|
166
|
-
const object = renderObject(schema.fields, 1, resolver);
|
|
232
|
+
const object = renderObject(schema.fields, 1, resolver, schema.description);
|
|
167
233
|
const bases = schema.bases ?? [];
|
|
168
234
|
const body = bases.length > 0
|
|
169
235
|
? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
|
package/docs/third-service.md
CHANGED
|
@@ -2,121 +2,151 @@
|
|
|
2
2
|
|
|
3
3
|
第三方服务适配器契约(如微信支付 tenpay、短信、文件存储)。`defineThirdService` 声明适配器类契约:构造函数配置 + 方法列表。
|
|
4
4
|
|
|
5
|
+
> 完整对接流程(前置准备 → 契约化 → gen third → 填充 client → 沙箱验证)见 [methodology/third-party-integration.md](../../docs/methodology/third-party-integration.md)。
|
|
6
|
+
|
|
7
|
+
与两个相近概念区分:
|
|
8
|
+
|
|
9
|
+
- `ServiceSchema`(service_schema/)——后端业务服务;
|
|
10
|
+
- `ThirdApiSchema`(project.config.ts `thirdApis`)——项目拓扑:第三方系统的目录归属,如 `wx/`、`ble/`。
|
|
11
|
+
|
|
12
|
+
`ThirdServiceSchema.schema` 引用 `ThirdApiSchema` 实例(拓扑引用)。第三方方法实现在外部系统,仅声明契约、不建模内部流程。
|
|
13
|
+
|
|
14
|
+
## 定义
|
|
15
|
+
|
|
5
16
|
```ts
|
|
6
|
-
import {
|
|
17
|
+
import { buildInput, buildOutput, CodeException, defineThirdService, dtoField, intField, IOException, stringField } from '@pylonts/dsl';
|
|
7
18
|
import { wx } from '../project.config';
|
|
8
19
|
|
|
9
|
-
const
|
|
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
|
-
});
|
|
20
|
+
const totalFee = intField({ optional: false, label: '金额(分)' });
|
|
16
21
|
|
|
17
22
|
export const wxPayService = defineThirdService({
|
|
18
23
|
schema: wx,
|
|
19
24
|
name: 'WxPayService',
|
|
20
25
|
description: '微信支付服务(tenpay APIv2)',
|
|
21
|
-
methods:
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
methods: {
|
|
27
|
+
getPayParams: {
|
|
28
|
+
args: buildInput('PayParams', {
|
|
29
|
+
out_trade_no: dtoField(stringField({ maxLength: 32, optional: false, label: '订单号' })),
|
|
30
|
+
total_fee: dtoField(totalFee),
|
|
31
|
+
// Same fact and same type as the entity column — shared instance.
|
|
32
|
+
openid: dtoField(user.columns.openid),
|
|
33
|
+
}),
|
|
34
|
+
results: buildOutput('PayParamsResult', {
|
|
35
|
+
appId: dtoField(stringField({ optional: false, label: 'appId' })),
|
|
36
|
+
paySign: dtoField(stringField({ optional: false, label: '签名' })),
|
|
37
|
+
}),
|
|
38
|
+
throws: [CodeException, IOException],
|
|
39
|
+
description: '获取支付参数',
|
|
34
40
|
},
|
|
35
|
-
|
|
41
|
+
},
|
|
36
42
|
});
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
- `methods` 是 **map**:key 即方法名(构建器写回 `method.name`),value 为 `ThirdServiceMethodDef`。
|
|
46
|
+
- 每个方法的 `args` / `results` 各是一个 `DtoMessage`,用 `buildInput` / `buildOutput` 构建。`buildInput` 构建 `args`(输入消息),`buildOutput` 构建 `results`(输出消息)——与业务 DTO 同一 POJO 聚合,字段以 `dtoField(field)` 包装,map key 即线格式(wire-format)字段名,**原样保留协议拼写**(`out_trade_no`、`appId`,不做 camelCase)。
|
|
47
|
+
- `throws`(**必填**):每个方法必须声明 **`CodeException` + `IOException`** 两个异常——`CodeException`(第三方返回的业务错误码)与 `IOException`(网络/超时/不可恢复故障)。两个异常都来自 `@pylonts/core`,**不能自定义、不能替换**(`pylonts gen third` 会校验,缺失即报错拒绝生成)。原因:第三方集成有两类必然失败——业务层失败(第三方返回错误码,需转译给调用方)与传输层失败(网络/超时,需按故障重试或上报),client 骨架的异常翻译依赖这两个契约。
|
|
48
|
+
- `description`(可选):服务或方法说明。
|
|
42
49
|
|
|
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
|
-
两个通道,按"同一事实"的表达方式选择:
|
|
50
|
+
字段与本地实体列/其他消息字段的关系,两个通道,按"同一事实"的表达方式选择:
|
|
55
51
|
|
|
56
52
|
### 同一概念且类型一致 → 共享实例
|
|
57
53
|
|
|
58
|
-
直接复用本地实体列实例,类型/语义/默认值自动跟随实体,DTO
|
|
54
|
+
直接复用本地实体列实例,类型/语义/默认值自动跟随实体,DTO 投影继承全部语义:
|
|
59
55
|
|
|
60
56
|
```ts
|
|
61
57
|
args: {
|
|
62
|
-
name: '
|
|
58
|
+
name: 'PayParams',
|
|
63
59
|
fields: {
|
|
64
|
-
openid: user.columns.openid, //
|
|
60
|
+
openid: dtoField(user.columns.openid), // user 是 TableSchema 实例,此处复用其 openid 列的 Field 对象
|
|
65
61
|
},
|
|
66
62
|
},
|
|
67
63
|
```
|
|
68
64
|
|
|
69
|
-
|
|
65
|
+
线格式字段名(map key)与列名无需一致——key 是协议拼写,value 是任意 Field 实例,两者解耦。协议叫 `userId`、列叫 `user_id` 照样共享:
|
|
70
66
|
|
|
71
|
-
|
|
67
|
+
```ts
|
|
68
|
+
fields: {
|
|
69
|
+
userId: dtoField(user.columns.user_id), // key 按协议拼写,value 复用列实例
|
|
70
|
+
},
|
|
71
|
+
```
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
> `user` 是 `schema/user.table.ts` 中 `export const user = defineTable('user', { ... })` 导出的 **TableSchema 实例**(`user.columns` 是它的列 map,`user.columns.openid` 是该表 `openid` 列的 Field 实例)。共享实例即把**同一个 Field 对象**放入消息字段,类型/语义/默认值全部跟随表定义。
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
// 规则 = 名称 + 两端(具名 map)。同一语义全局只允许一条(重复定义抛错)。
|
|
77
|
-
const fenYuan = defineFieldRule({
|
|
78
|
-
name: 'fenYuan',
|
|
79
|
-
ends: { fen: {}, yuan: {} },
|
|
80
|
-
});
|
|
75
|
+
### 同一概念但类型/格式不同 → 自有字段
|
|
81
76
|
|
|
77
|
+
声明自有线格式类型:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
82
80
|
const totalFee = intField({ optional: false, label: '金额(分)' });
|
|
83
81
|
|
|
84
82
|
fields: {
|
|
85
|
-
total_fee: totalFee,
|
|
83
|
+
total_fee: dtoField(totalFee),
|
|
86
84
|
},
|
|
87
|
-
refs: [
|
|
88
|
-
{
|
|
89
|
-
field: totalFee, // 本地定义(本消息的 wire 字段)
|
|
90
|
-
ref: order.columns.amount, // 其他定义(表列或其他消息字段)
|
|
91
|
-
convert: { rule: fenYuan, end: fenYuan.ends.fen },
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
85
|
```
|
|
95
86
|
|
|
96
|
-
|
|
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 端)
|
|
87
|
+
wire 字段与本地字段之间的换算/映射(分↔元、加密、脱敏)由 convert 防腐层承载,`FieldRuleSchema` 换算规则为规划能力、尚未接入消息绑定。
|
|
102
88
|
|
|
103
89
|
## 嵌套字段
|
|
104
90
|
|
|
105
|
-
线格式字段支持递归嵌套,用 `
|
|
91
|
+
线格式字段支持递归嵌套,用 `objectField` / `arrayField`(Field 体系,非表列):
|
|
106
92
|
|
|
107
93
|
```ts
|
|
108
94
|
fields: {
|
|
109
|
-
payer_info: objectField({
|
|
95
|
+
payer_info: dtoField(objectField({
|
|
110
96
|
properties: {
|
|
111
97
|
openid: stringField({ optional: false, maxLength: 64 }),
|
|
112
98
|
},
|
|
113
|
-
}),
|
|
114
|
-
coupons: arrayField({ items: intField() }),
|
|
99
|
+
})),
|
|
100
|
+
coupons: dtoField(arrayField({ items: intField() })),
|
|
115
101
|
},
|
|
116
102
|
```
|
|
117
103
|
|
|
118
|
-
表列不支持这两个类型(`buildCreateTableSql`
|
|
104
|
+
表列不支持这两个类型(`buildCreateTableSql` 直接报错,定义期即拦截)。
|
|
105
|
+
|
|
106
|
+
## 枚举与异常
|
|
107
|
+
|
|
108
|
+
第三方消息字段可用 `enumField` 挂 `defineEnum` 枚举,两者与 `defineThirdService` 定义在同一源文件中(named export),供 `pylonts gen third` 生成枚举产物。
|
|
109
|
+
|
|
110
|
+
异常不在此处定义——方法 `throws` 固定声明 `@pylonts/core` 的 `CodeException` + `IOException`(见上文「定义」一节),`pylonts gen third` 强校验。
|
|
111
|
+
|
|
112
|
+
## 存储与生成
|
|
113
|
+
|
|
114
|
+
- 声明:`third_schema/{thirdApi.name}/*.third-service.ts`——一文件一服务(named export),目录名 = project.config.ts 的 `thirdApis` 实例名。
|
|
115
|
+
- 生成:`pylonts gen third`——对每个 thirdApi,扫描 `third_schema/{name}/`,按三步产出到 `third/{name}/`:
|
|
116
|
+
0. **throws 校验**(生成前置闸门):每个方法必须声明 `CodeException` + `IOException`,缺失即报错列出违规方法,不写任何产物;
|
|
117
|
+
1. **枚举**:模块导出的 `defineEnum` 实例 → `third/{name}/enums/{JsName}.enum.ts`(复用 enum-driver 的 `renderEnum`,与表枚举同一渲染),一 jsName 一文件;
|
|
118
|
+
2. **DTO**:每方法 args/results 用 typebox-driver 渲染 TypeBox 消息 + Static 类型,**一源文件一生成文件**,输出 `third/{name}/{stem}.third-service.gen.ts`(覆盖写);
|
|
119
|
+
3. **客户端骨架**:每服务渲染一个 class(构造配置接口 + 每方法 async 签名 + Not-implemented throw),输出 `third/{name}/{stem}.client.ts`——**已存在则跳过**(方法体是用户填充的),`--force` 覆盖。
|
|
120
|
+
- DTO 的枚举字段 import 走**相对路径** `./enums/{JsName}.enum`(DTO 与 enums/ 同处 `third/{name}/` 下,`moduleResolution: bundler` 解析 `.enum.ts`),不依赖根 `enums/` 子包。
|
|
121
|
+
- 生成物目录是子包:`third/` 目录带 `package.json`,`exports` 声明 `*.third-service.gen` 子路径,供 convert 产物 import。
|
|
122
|
+
|
|
123
|
+
## 客户端骨架是生成的,方法体是手写的
|
|
119
124
|
|
|
120
|
-
|
|
125
|
+
`defineThirdService` 只描述**契约**(构造配置 + 方法列表)。`gen third` 生成的 `{name}.client.ts` 是一个**骨架**:导出 `{Service}Config` 接口(TODO 注释标注 transport 配置——baseUrl/凭据/密钥属外部实现,不在契约内)+ `{Service}` 类(constructor 空实现),每方法带完整签名(args/results 类型从同名 `.third-service.gen` import type)与 `throw new Error('Not implemented: ...')` stub(含 `// @gen:stub` 标记,与 gen-service 骨架同一套 marker 约定)。**签名/throws 注释由生成器保证与契约同步,方法体、构造配置、签名加密等外部交互由人工填充**——已存在文件默认跳过(避免覆盖人工实现),`--force` 才重写。
|
|
126
|
+
|
|
127
|
+
## convert 防腐接线
|
|
128
|
+
|
|
129
|
+
第三方消息(`args`/`results` 是 `DtoMessage`,天然满足 `ConvertSourceSchema`)可直接作为 convert 的**源或目标**,用于 wire 消息 ↔ 本地模型的防腐翻译。```ts
|
|
130
|
+
// convert_schema/{api.name}/{app.name}/convert/wx-pay.convert.ts
|
|
131
|
+
import { wxPayService } from '../../../third_schema/wx/wxpay.third-service';
|
|
132
|
+
|
|
133
|
+
const getPayParams = wxPayService.methods.getPayParams;
|
|
134
|
+
|
|
135
|
+
export const wxPayConvert = defineConvert({
|
|
136
|
+
name: 'WxPayConvert',
|
|
137
|
+
api,
|
|
138
|
+
app: admin,
|
|
139
|
+
methods: {
|
|
140
|
+
toPayParams: {
|
|
141
|
+
sources: [order], // 本地订单表 → wire 请求
|
|
142
|
+
target: getPayParams.args,
|
|
143
|
+
},
|
|
144
|
+
toLocalPayResult: {
|
|
145
|
+
sources: [getPayParams.results], // wire 响应 → 本地 DTO
|
|
146
|
+
target: WxPayParamsResultDto,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
121
151
|
|
|
122
|
-
|
|
152
|
+
convert 文件绑定第三方服务身份时按 `{third-service}.convert.ts` 命名(上例 `wx-pay.convert.ts` 对应 `wxpay.third-service.ts` 的 `WxPayService`)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonts/dsl",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"author": "",
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@pylonts/core": "^1.1.
|
|
44
|
+
"@pylonts/core": "^1.1.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"typescript": "^7.0.2",
|
package/src/typebox-driver.ts
CHANGED
|
@@ -1,235 +1,304 @@
|
|
|
1
|
-
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef, isDtoField, isDtoMessage } from './dto.js';
|
|
2
|
-
import { collectEnumRefs, EnumField, Field } from './dsl.js';
|
|
3
|
-
|
|
4
|
-
// TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
|
|
5
|
-
// Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
|
|
6
|
-
//
|
|
7
|
-
// export const RegisterUserInput = Type.Object({...});
|
|
8
|
-
// export type RegisterUserInput = Static<typeof RegisterUserInput>;
|
|
9
|
-
//
|
|
10
|
-
// ENUM fields reference a generated enum (see enum-driver) by its import
|
|
11
|
-
// location — the resolver maps a jsName to its product import.
|
|
12
|
-
// DTO bases (.include()) render as Type.Intersect([...bases, Type.Object({...})]).
|
|
13
|
-
|
|
14
|
-
export type EnumResolver = (enumName: string) => ImportBase | undefined;
|
|
15
|
-
|
|
16
|
-
function renderString(s: string): string {
|
|
17
|
-
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderDefault(v: unknown): string {
|
|
21
|
-
if (typeof v === 'string') return renderString(v);
|
|
22
|
-
return JSON.stringify(v);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
): string {
|
|
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
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
if (def !== undefined)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
case '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
export
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
1
|
+
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef, isDtoField, isDtoMessage } from './dto.js';
|
|
2
|
+
import { collectEnumRefs, EnumField, Field } from './dsl.js';
|
|
3
|
+
|
|
4
|
+
// TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
|
|
5
|
+
// Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
|
|
6
|
+
//
|
|
7
|
+
// export const RegisterUserInput = Type.Object({...});
|
|
8
|
+
// export type RegisterUserInput = Static<typeof RegisterUserInput>;
|
|
9
|
+
//
|
|
10
|
+
// ENUM fields reference a generated enum (see enum-driver) by its import
|
|
11
|
+
// location — the resolver maps a jsName to its product import.
|
|
12
|
+
// DTO bases (.include()) render as Type.Intersect([...bases, Type.Object({...})]).
|
|
13
|
+
|
|
14
|
+
export type EnumResolver = (enumName: string) => ImportBase | undefined;
|
|
15
|
+
|
|
16
|
+
function renderString(s: string): string {
|
|
17
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderDefault(v: unknown): string {
|
|
21
|
+
if (typeof v === 'string') return renderString(v);
|
|
22
|
+
return JSON.stringify(v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** BaseField-level description: description takes priority over label. */
|
|
26
|
+
function fieldDescription(field: { description?: string; label?: string }): string | undefined {
|
|
27
|
+
return field.description ?? field.label;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** DtoField-level description: own description > referenced field description/label. */
|
|
31
|
+
function dtoFieldDescription(f: DtoField): string | undefined {
|
|
32
|
+
return f.description ?? fieldDescription(f.field);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderBasic(
|
|
36
|
+
field: Field,
|
|
37
|
+
pattern: string | undefined,
|
|
38
|
+
defaultValue: unknown,
|
|
39
|
+
resolver: EnumResolver | undefined,
|
|
40
|
+
indent = 0,
|
|
41
|
+
description?: string,
|
|
42
|
+
): string {
|
|
43
|
+
if (pattern !== undefined && field.type !== 'string') {
|
|
44
|
+
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
45
|
+
}
|
|
46
|
+
const def = defaultValue !== undefined ? `default: ${renderDefault(defaultValue)}` : undefined;
|
|
47
|
+
const desc = description !== undefined ? `description: ${renderString(description)}` : undefined;
|
|
48
|
+
const withOpts = (base: string, opts: string[]): string =>
|
|
49
|
+
opts.length > 0 ? `${base}({ ${opts.join(', ')} })` : `${base}()`;
|
|
50
|
+
switch (field.type) {
|
|
51
|
+
case 'string': {
|
|
52
|
+
const opts: string[] = [];
|
|
53
|
+
if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
|
|
54
|
+
if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
|
|
55
|
+
if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
|
|
56
|
+
if (def !== undefined) opts.push(def);
|
|
57
|
+
if (desc !== undefined) opts.push(desc);
|
|
58
|
+
return withOpts('Type.String', opts);
|
|
59
|
+
}
|
|
60
|
+
case 'text': {
|
|
61
|
+
const opts: string[] = [];
|
|
62
|
+
if (def !== undefined) opts.push(def);
|
|
63
|
+
if (desc !== undefined) opts.push(desc);
|
|
64
|
+
return withOpts('Type.String', opts);
|
|
65
|
+
}
|
|
66
|
+
case 'integer': {
|
|
67
|
+
const opts: string[] = [];
|
|
68
|
+
if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
|
|
69
|
+
if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
|
|
70
|
+
if (def !== undefined) opts.push(def);
|
|
71
|
+
if (desc !== undefined) opts.push(desc);
|
|
72
|
+
return withOpts('Type.Integer', opts);
|
|
73
|
+
}
|
|
74
|
+
case 'bigint':
|
|
75
|
+
case 'decimal':
|
|
76
|
+
case 'rate':
|
|
77
|
+
case 'time':
|
|
78
|
+
case 'date':
|
|
79
|
+
case 'datetime': {
|
|
80
|
+
// Transmitted as string over HTTP: bigint/decimal/rate keep full
|
|
81
|
+
// precision, date/time serialize to string.
|
|
82
|
+
const opts: string[] = [];
|
|
83
|
+
if (def !== undefined) opts.push(def);
|
|
84
|
+
if (desc !== undefined) opts.push(desc);
|
|
85
|
+
return withOpts('Type.String', opts);
|
|
86
|
+
}
|
|
87
|
+
case 'boolean': {
|
|
88
|
+
const opts: string[] = [];
|
|
89
|
+
if (def !== undefined) opts.push(def);
|
|
90
|
+
if (desc !== undefined) opts.push(desc);
|
|
91
|
+
return withOpts('Type.Boolean', opts);
|
|
92
|
+
}
|
|
93
|
+
case 'json': {
|
|
94
|
+
const opts: string[] = [];
|
|
95
|
+
if (def !== undefined) opts.push(def);
|
|
96
|
+
if (desc !== undefined) opts.push(desc);
|
|
97
|
+
return withOpts('Type.Unknown', opts);
|
|
98
|
+
}
|
|
99
|
+
case 'enum': {
|
|
100
|
+
const ref = resolver?.(field.enum.jsName);
|
|
101
|
+
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
|
|
102
|
+
if (def !== undefined) {
|
|
103
|
+
const member = field.enum.values.find((v) => v.value === defaultValue);
|
|
104
|
+
if (!member) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`enum field ${field.name}: default ${renderDefault(defaultValue)} is not a member of ${field.enum.jsName}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const opts: string[] = [];
|
|
110
|
+
if (desc !== undefined) opts.push(desc);
|
|
111
|
+
return `Type.Enum(${ref.name}, { default: ${ref.name}.${member.symbol}${opts.length > 0 ? `, ${opts.join(', ')}` : ''} })`;
|
|
112
|
+
}
|
|
113
|
+
if (desc !== undefined) return `Type.Enum(${ref.name}, { ${desc} })`;
|
|
114
|
+
return `Type.Enum(${ref.name})`;
|
|
115
|
+
}
|
|
116
|
+
case 'aggregate': {
|
|
117
|
+
// Aggregate query outputs: count/sum(int) are numbers, everything else
|
|
118
|
+
// arrives as a precision string.
|
|
119
|
+
const opts: string[] = [];
|
|
120
|
+
if (desc !== undefined) opts.push(desc);
|
|
121
|
+
return field.jsType === 'number' ? withOpts('Type.Number', opts) : withOpts('Type.String', opts);
|
|
122
|
+
}
|
|
123
|
+
case 'array':
|
|
124
|
+
return desc !== undefined
|
|
125
|
+
? `Type.Array(${renderFieldValue(field.items, indent, resolver)}, { ${desc} })`
|
|
126
|
+
: `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
|
|
127
|
+
case 'object':
|
|
128
|
+
return renderFieldObject(field.properties, indent + 1, resolver, description);
|
|
129
|
+
default:
|
|
130
|
+
// Field union is exhaustive; this branch is unreachable at runtime.
|
|
131
|
+
throw new Error(`unsupported field type: ${String((field as Field).type)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Render a plain Field value (wire-format nested fields), wrapping optional. */
|
|
136
|
+
function renderFieldValue(field: Field, indent: number, resolver: EnumResolver | undefined): string {
|
|
137
|
+
const base = renderBasic(field, undefined, undefined, resolver, indent, fieldDescription(field));
|
|
138
|
+
return field.optional ? `Type.Optional(${base})` : base;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Render a plain Field object (wire-format nested object). */
|
|
142
|
+
function renderFieldObject(
|
|
143
|
+
properties: Record<string, Field>,
|
|
144
|
+
indent: number,
|
|
145
|
+
resolver: EnumResolver | undefined,
|
|
146
|
+
description?: string,
|
|
147
|
+
): string {
|
|
148
|
+
const pad = ' '.repeat(indent);
|
|
149
|
+
const entries = Object.entries(properties).map(([name, f]) => `${pad}${name}: ${renderFieldValue(f, indent, resolver)}`);
|
|
150
|
+
const obj = `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
151
|
+
return description !== undefined
|
|
152
|
+
? `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}}, { description: ${renderString(description)} })`
|
|
153
|
+
: obj;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderObject(
|
|
157
|
+
fields: Record<string, DtoField | Field>,
|
|
158
|
+
indent: number,
|
|
159
|
+
resolver: EnumResolver | undefined,
|
|
160
|
+
description?: string,
|
|
161
|
+
): string {
|
|
162
|
+
const pad = ' '.repeat(indent);
|
|
163
|
+
const entries = Object.entries(fields).map(([name, f]) => {
|
|
164
|
+
const rendered = isDtoField(f) ? renderField(f, indent, resolver) : renderFieldValue(f, indent, resolver);
|
|
165
|
+
return `${pad}${name}: ${rendered}`;
|
|
166
|
+
});
|
|
167
|
+
const obj = `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
168
|
+
return description !== undefined
|
|
169
|
+
? `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}}, { description: ${renderString(description)} })`
|
|
170
|
+
: obj;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
174
|
+
const base = renderValue(f, indent, resolver);
|
|
175
|
+
return f.isOptional() ? `Type.Optional(${base})` : base;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
179
|
+
if (f.field.type === 'array') {
|
|
180
|
+
const items = f.field.items;
|
|
181
|
+
const desc = dtoFieldDescription(f);
|
|
182
|
+
// Referenced DTO element — render by name (same-file export), not expanded.
|
|
183
|
+
if (isDtoMessage(items)) {
|
|
184
|
+
return desc !== undefined
|
|
185
|
+
? `Type.Array(${items.name}, { description: ${renderString(desc)} })`
|
|
186
|
+
: `Type.Array(${items.name})`;
|
|
187
|
+
}
|
|
188
|
+
if (isDtoField(items)) {
|
|
189
|
+
const rendered = `Type.Array(${renderField(items, indent + 1, resolver)})`;
|
|
190
|
+
return desc !== undefined
|
|
191
|
+
? `Type.Array(${renderField(items, indent + 1, resolver)}, { description: ${renderString(desc)} })`
|
|
192
|
+
: rendered;
|
|
193
|
+
}
|
|
194
|
+
const rendered = `Type.Array(${renderFieldValue(items, indent, resolver)})`;
|
|
195
|
+
return desc !== undefined
|
|
196
|
+
? `Type.Array(${renderFieldValue(items, indent, resolver)}, { description: ${renderString(desc)} })`
|
|
197
|
+
: rendered;
|
|
198
|
+
}
|
|
199
|
+
if (f.field.type === 'object') {
|
|
200
|
+
return renderObject(f.field.properties, indent + 1, resolver, dtoFieldDescription(f));
|
|
201
|
+
}
|
|
202
|
+
// DtoField only wraps a database Field; array/object defs live in the subclasses.
|
|
203
|
+
// Only DTO-level defaults (setDefault) are emitted as TypeBox default
|
|
204
|
+
// annotations; DB field defaults are not carried into the API contract.
|
|
205
|
+
return renderBasic(f.field as Field, f.pattern, f.default, resolver, indent, dtoFieldDescription(f));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectEnumImports(
|
|
209
|
+
f: DtoField,
|
|
210
|
+
resolver: EnumResolver | undefined,
|
|
211
|
+
out: Map<string, ImportBase>,
|
|
212
|
+
): void {
|
|
213
|
+
if (f.field.type === 'array') {
|
|
214
|
+
const items = f.field.items;
|
|
215
|
+
if (isDtoMessage(items)) return;
|
|
216
|
+
if (isDtoField(items)) {
|
|
217
|
+
collectEnumImports(items, resolver, out);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
collectFieldEnumImports(items, resolver, out);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (f.field.type === 'object') {
|
|
224
|
+
for (const child of Object.values(f.field.properties)) {
|
|
225
|
+
if (isDtoField(child)) collectEnumImports(child, resolver, out);
|
|
226
|
+
else collectFieldEnumImports(child, resolver, out);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (f.field.type === 'enum') collectEnumRef(f.field, resolver, out);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Enum import collection over a plain Field (wire-format nested fields). */
|
|
234
|
+
function collectFieldEnumImports(
|
|
235
|
+
field: Field,
|
|
236
|
+
resolver: EnumResolver | undefined,
|
|
237
|
+
out: Map<string, ImportBase>,
|
|
238
|
+
): void {
|
|
239
|
+
for (const jsName of collectEnumRefs(field)) {
|
|
240
|
+
const ref = resolver?.(jsName);
|
|
241
|
+
if (!ref) throw new Error(`enum ${jsName}: no import ref — pass an EnumResolver`);
|
|
242
|
+
out.set(`${ref.from}#${ref.name}`, ref);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function collectEnumRef(field: EnumField, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void {
|
|
247
|
+
collectFieldEnumImports(field, resolver, out);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Collect all imports needed to render a DTO: include() bases + enum references. */
|
|
251
|
+
export function collectDtoImports(
|
|
252
|
+
schema: DtoMessage,
|
|
253
|
+
resolver: EnumResolver | undefined,
|
|
254
|
+
out: Map<string, ImportBase>,
|
|
255
|
+
): void {
|
|
256
|
+
for (const base of schema.bases ?? []) out.set(`${base.from}#${base.name}`, base);
|
|
257
|
+
for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, out);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Render one DTO export (const + type) — no file header, for file-level generation. */
|
|
261
|
+
export function renderDtoExport(schema: DtoMessage, resolver: EnumResolver | undefined): string {
|
|
262
|
+
const object = renderObject(schema.fields, 1, resolver, schema.description);
|
|
263
|
+
const bases = schema.bases ?? [];
|
|
264
|
+
const body =
|
|
265
|
+
bases.length > 0
|
|
266
|
+
? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
|
|
267
|
+
: object;
|
|
268
|
+
return `export const ${schema.name} = ${body};`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Render the Static type export for a DTO. */
|
|
272
|
+
export function renderDtoTypeExport(name: string): string {
|
|
273
|
+
return `export type ${name} = Static<typeof ${name}>;`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function renderDtoMessage(
|
|
277
|
+
schema: DtoMessage,
|
|
278
|
+
options: { resolver?: EnumResolver; source?: string } = {},
|
|
279
|
+
): string {
|
|
280
|
+
const { resolver, source } = options;
|
|
281
|
+
const imports = new Map<string, ImportBase>();
|
|
282
|
+
collectDtoImports(schema, resolver, imports);
|
|
283
|
+
|
|
284
|
+
const header = [
|
|
285
|
+
'// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
|
|
286
|
+
...(source !== undefined ? [`// Source: ${source}`] : []),
|
|
287
|
+
"import { Type, Static } from '@sinclair/typebox';",
|
|
288
|
+
...[...imports.values()].map((r) => `import${r.type ? ' type' : ''} { ${r.name} } from '${r.from}';`),
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
return [
|
|
292
|
+
...header,
|
|
293
|
+
'',
|
|
294
|
+
renderDtoExport(schema, resolver),
|
|
295
|
+
renderDtoTypeExport(schema.name),
|
|
296
|
+
'',
|
|
297
|
+
].join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderBase(base: ImportRef): string {
|
|
301
|
+
if (base.args === undefined || base.args.length === 0) return base.name;
|
|
302
|
+
const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
|
|
303
|
+
return `${base.name}(${args.join(', ')})`;
|
|
235
304
|
}
|