@pylonts/dsl 1.0.2 → 1.0.3
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/docs/dictionary.md +14 -3
- package/docs/dto.md +8 -2
- package/docs/table.md +12 -3
- package/package.json +1 -1
- package/src/dictionary.ts +6 -1
- package/src/typebox-driver.ts +140 -140
package/dist/dictionary.d.ts
CHANGED
|
@@ -4,5 +4,10 @@ export interface DictionaryEntry extends SchemaBase {
|
|
|
4
4
|
/** Display label (Chinese) for the term. */
|
|
5
5
|
label?: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Explains a phrase — the returned entry IS the phrase. `name` is the phrase
|
|
9
|
+
* itself (column-name stem, e.g. mer / bd / amt), not a full entity name;
|
|
10
|
+
* label/description explain what it means. Convention: short phrase, not long
|
|
11
|
+
* name.
|
|
12
|
+
*/
|
|
8
13
|
export declare function definePhrase(extra: DictionaryEntry): DictionaryEntry;
|
package/dist/dictionary.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.definePhrase = definePhrase;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Explains a phrase — the returned entry IS the phrase. `name` is the phrase
|
|
6
|
+
* itself (column-name stem, e.g. mer / bd / amt), not a full entity name;
|
|
7
|
+
* label/description explain what it means. Convention: short phrase, not long
|
|
8
|
+
* name.
|
|
9
|
+
*/
|
|
5
10
|
function definePhrase(extra) {
|
|
6
11
|
return extra;
|
|
7
12
|
}
|
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
|
@@ -15,7 +15,7 @@ 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]));
|
|
@@ -23,7 +23,7 @@ buildOutput('OrderRow', from(order, [order.fields.id, order.fields.order_no]));
|
|
|
23
23
|
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
24
24
|
buildQuery('OrderPageQuery', {
|
|
25
25
|
keyword: dtoField(stringField({ maxLength: 32 })).op('like'),
|
|
26
|
-
...from(order, [order.fields.
|
|
26
|
+
...from(order, [order.fields.mer_id]),
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
// 主键:按 id 取详情
|
|
@@ -52,3 +52,9 @@ buildQuery('OrderPageQuery', { ... })
|
|
|
52
52
|
- **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
|
|
53
53
|
- **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
|
|
54
54
|
- **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
|
|
55
|
+
|
|
56
|
+
## 文件组织
|
|
57
|
+
|
|
58
|
+
- `dto_schema/{module}/*.dto.ts`:DTO 源文件,一个文件可导出多个 DtoMessage;`module` = `project.config.ts` 的 `apps.name`,另加 `common/` 放跨 app 共享 DTO。
|
|
59
|
+
- `gen-cli dto` 按 module 逐个扫描:`dto_schema/{module}/*.dto.ts` → 生成 `dto/{module}/{Name}.dto.ts`。
|
|
60
|
+
- 枚举字段引用 `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/typebox-driver.ts
CHANGED
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportRef } from './dto';
|
|
2
|
-
import { Field } from './dsl';
|
|
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) => ImportRef | undefined;
|
|
15
|
-
|
|
16
|
-
function renderString(s: string): string {
|
|
17
|
-
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderBasic(field: Field, pattern: string | undefined, resolver: EnumResolver | undefined): string {
|
|
21
|
-
if (pattern !== undefined && field.type !== 'string') {
|
|
22
|
-
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
23
|
-
}
|
|
24
|
-
switch (field.type) {
|
|
25
|
-
case 'string': {
|
|
26
|
-
const opts: string[] = [];
|
|
27
|
-
if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
|
|
28
|
-
if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
|
|
29
|
-
if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
|
|
30
|
-
return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
|
|
31
|
-
}
|
|
32
|
-
case 'text':
|
|
33
|
-
return 'Type.String()';
|
|
34
|
-
case 'integer': {
|
|
35
|
-
const opts: string[] = [];
|
|
36
|
-
if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
|
|
37
|
-
if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
|
|
38
|
-
return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
|
|
39
|
-
}
|
|
40
|
-
case 'bigint':
|
|
41
|
-
case 'decimal':
|
|
42
|
-
case 'time':
|
|
43
|
-
case 'date':
|
|
44
|
-
case 'datetime':
|
|
45
|
-
// Transmitted as string over HTTP: bigint/decimal keep full precision,
|
|
46
|
-
// date/time serialize to string.
|
|
47
|
-
return 'Type.String()';
|
|
48
|
-
case 'boolean':
|
|
49
|
-
return 'Type.Boolean()';
|
|
50
|
-
case 'json':
|
|
51
|
-
return 'Type.Unknown()';
|
|
52
|
-
case 'enum': {
|
|
53
|
-
const ref = resolver?.(field.enum.jsName);
|
|
54
|
-
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
|
|
55
|
-
return `Type.Enum(${ref.name})`;
|
|
56
|
-
}
|
|
57
|
-
default:
|
|
58
|
-
// Field union is exhaustive; this branch is unreachable at runtime.
|
|
59
|
-
throw new Error(`unsupported field type: ${String((field as Field).type)}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function renderObject(fields: Record<string, DtoField>, indent: number, resolver: EnumResolver | undefined): string {
|
|
64
|
-
const pad = ' '.repeat(indent);
|
|
65
|
-
const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
|
|
66
|
-
return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
70
|
-
const base = renderValue(f, indent, resolver);
|
|
71
|
-
return f.isOptional() ? `Type.Optional(${base})` : base;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
75
|
-
if (f instanceof DtoArrayField) {
|
|
76
|
-
return `Type.Array(${renderField(f.items(), indent + 1, resolver)})`;
|
|
77
|
-
}
|
|
78
|
-
if (f instanceof DtoObjectField) {
|
|
79
|
-
return renderObject(f.properties(), indent + 1, resolver);
|
|
80
|
-
}
|
|
81
|
-
// DtoField only wraps a database Field; array/object defs live in the subclasses.
|
|
82
|
-
return renderBasic(f.field as Field, f.pattern, resolver);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function collectEnumImports(
|
|
86
|
-
f: DtoField,
|
|
87
|
-
resolver: EnumResolver | undefined,
|
|
88
|
-
out: Map<string, ImportRef>,
|
|
89
|
-
): void {
|
|
90
|
-
if (f instanceof DtoArrayField) {
|
|
91
|
-
collectEnumImports(f.items(), resolver, out);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (f instanceof DtoObjectField) {
|
|
95
|
-
for (const child of Object.values(f.properties())) collectEnumImports(child, resolver, out);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (f.field.type === 'enum') {
|
|
99
|
-
const ref = resolver?.(f.field.enum.jsName);
|
|
100
|
-
if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
|
|
101
|
-
out.set(`${ref.from}#${ref.name}`, ref);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function renderDtoMessage(
|
|
106
|
-
schema: DtoMessage,
|
|
107
|
-
options: { resolver?: EnumResolver; source?: string } = {},
|
|
108
|
-
): string {
|
|
109
|
-
const { resolver, source } = options;
|
|
110
|
-
const imports = new Map<string, ImportRef>();
|
|
111
|
-
for (const base of schema.bases ?? []) imports.set(`${base.from}#${base.name}`, base);
|
|
112
|
-
for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, imports);
|
|
113
|
-
|
|
114
|
-
const header = [
|
|
115
|
-
'// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
|
|
116
|
-
...(source !== undefined ? [`// Source: ${source}`] : []),
|
|
117
|
-
"import { Type, Static } from '@sinclair/typebox';",
|
|
118
|
-
...[...imports.values()].map((r) => `import { ${r.name} } from '${r.from}';`),
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
const object = renderObject(schema.fields, 1, resolver);
|
|
122
|
-
const bases = schema.bases ?? [];
|
|
123
|
-
const body =
|
|
124
|
-
bases.length > 0
|
|
125
|
-
? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
|
|
126
|
-
: object;
|
|
127
|
-
|
|
128
|
-
return [
|
|
129
|
-
...header,
|
|
130
|
-
'',
|
|
131
|
-
`export const ${schema.name} = ${body};`,
|
|
132
|
-
`export type ${schema.name} = Static<typeof ${schema.name}>;`,
|
|
133
|
-
'',
|
|
134
|
-
].join('\n');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function renderBase(base: ImportRef): string {
|
|
138
|
-
return base.args !== undefined && base.args.length > 0
|
|
139
|
-
? `${base.name}(${base.args.join(', ')})`
|
|
140
|
-
: base.name;
|
|
1
|
+
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportRef } from './dto';
|
|
2
|
+
import { Field } from './dsl';
|
|
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) => ImportRef | undefined;
|
|
15
|
+
|
|
16
|
+
function renderString(s: string): string {
|
|
17
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderBasic(field: Field, pattern: string | undefined, resolver: EnumResolver | undefined): string {
|
|
21
|
+
if (pattern !== undefined && field.type !== 'string') {
|
|
22
|
+
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
23
|
+
}
|
|
24
|
+
switch (field.type) {
|
|
25
|
+
case 'string': {
|
|
26
|
+
const opts: string[] = [];
|
|
27
|
+
if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
|
|
28
|
+
if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
|
|
29
|
+
if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
|
|
30
|
+
return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
|
|
31
|
+
}
|
|
32
|
+
case 'text':
|
|
33
|
+
return 'Type.String()';
|
|
34
|
+
case 'integer': {
|
|
35
|
+
const opts: string[] = [];
|
|
36
|
+
if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
|
|
37
|
+
if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
|
|
38
|
+
return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
|
|
39
|
+
}
|
|
40
|
+
case 'bigint':
|
|
41
|
+
case 'decimal':
|
|
42
|
+
case 'time':
|
|
43
|
+
case 'date':
|
|
44
|
+
case 'datetime':
|
|
45
|
+
// Transmitted as string over HTTP: bigint/decimal keep full precision,
|
|
46
|
+
// date/time serialize to string.
|
|
47
|
+
return 'Type.String()';
|
|
48
|
+
case 'boolean':
|
|
49
|
+
return 'Type.Boolean()';
|
|
50
|
+
case 'json':
|
|
51
|
+
return 'Type.Unknown()';
|
|
52
|
+
case 'enum': {
|
|
53
|
+
const ref = resolver?.(field.enum.jsName);
|
|
54
|
+
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
|
|
55
|
+
return `Type.Enum(${ref.name})`;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
// Field union is exhaustive; this branch is unreachable at runtime.
|
|
59
|
+
throw new Error(`unsupported field type: ${String((field as Field).type)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderObject(fields: Record<string, DtoField>, indent: number, resolver: EnumResolver | undefined): string {
|
|
64
|
+
const pad = ' '.repeat(indent);
|
|
65
|
+
const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
|
|
66
|
+
return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
70
|
+
const base = renderValue(f, indent, resolver);
|
|
71
|
+
return f.isOptional() ? `Type.Optional(${base})` : base;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
75
|
+
if (f instanceof DtoArrayField) {
|
|
76
|
+
return `Type.Array(${renderField(f.items(), indent + 1, resolver)})`;
|
|
77
|
+
}
|
|
78
|
+
if (f instanceof DtoObjectField) {
|
|
79
|
+
return renderObject(f.properties(), indent + 1, resolver);
|
|
80
|
+
}
|
|
81
|
+
// DtoField only wraps a database Field; array/object defs live in the subclasses.
|
|
82
|
+
return renderBasic(f.field as Field, f.pattern, resolver);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function collectEnumImports(
|
|
86
|
+
f: DtoField,
|
|
87
|
+
resolver: EnumResolver | undefined,
|
|
88
|
+
out: Map<string, ImportRef>,
|
|
89
|
+
): void {
|
|
90
|
+
if (f instanceof DtoArrayField) {
|
|
91
|
+
collectEnumImports(f.items(), resolver, out);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (f instanceof DtoObjectField) {
|
|
95
|
+
for (const child of Object.values(f.properties())) collectEnumImports(child, resolver, out);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (f.field.type === 'enum') {
|
|
99
|
+
const ref = resolver?.(f.field.enum.jsName);
|
|
100
|
+
if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
|
|
101
|
+
out.set(`${ref.from}#${ref.name}`, ref);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function renderDtoMessage(
|
|
106
|
+
schema: DtoMessage,
|
|
107
|
+
options: { resolver?: EnumResolver; source?: string } = {},
|
|
108
|
+
): string {
|
|
109
|
+
const { resolver, source } = options;
|
|
110
|
+
const imports = new Map<string, ImportRef>();
|
|
111
|
+
for (const base of schema.bases ?? []) imports.set(`${base.from}#${base.name}`, base);
|
|
112
|
+
for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, imports);
|
|
113
|
+
|
|
114
|
+
const header = [
|
|
115
|
+
'// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
|
|
116
|
+
...(source !== undefined ? [`// Source: ${source}`] : []),
|
|
117
|
+
"import { Type, Static } from '@sinclair/typebox';",
|
|
118
|
+
...[...imports.values()].map((r) => `import { ${r.name} } from '${r.from}';`),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const object = renderObject(schema.fields, 1, resolver);
|
|
122
|
+
const bases = schema.bases ?? [];
|
|
123
|
+
const body =
|
|
124
|
+
bases.length > 0
|
|
125
|
+
? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
|
|
126
|
+
: object;
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
...header,
|
|
130
|
+
'',
|
|
131
|
+
`export const ${schema.name} = ${body};`,
|
|
132
|
+
`export type ${schema.name} = Static<typeof ${schema.name}>;`,
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderBase(base: ImportRef): string {
|
|
138
|
+
return base.args !== undefined && base.args.length > 0
|
|
139
|
+
? `${base.name}(${base.args.join(', ')})`
|
|
140
|
+
: base.name;
|
|
141
141
|
}
|