@pylonts/dsl 1.1.11 → 1.1.13

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.
Files changed (50) hide show
  1. package/dist/convert.d.ts +6 -8
  2. package/dist/curd.js +1 -1
  3. package/dist/dao.d.ts +10 -7
  4. package/dist/dao.js +20 -7
  5. package/dist/dsl.d.ts +18 -1
  6. package/dist/dsl.js +40 -0
  7. package/dist/dto.d.ts +13 -8
  8. package/dist/dto.js +68 -13
  9. package/dist/entity.d.ts +4 -3
  10. package/dist/entity.js +1 -1
  11. package/dist/filter.d.ts +6 -4
  12. package/dist/filter.js +1 -1
  13. package/dist/flow-script.js +8 -2
  14. package/dist/flow.d.ts +10 -2
  15. package/dist/flow.js +44 -4
  16. package/dist/mermaid-driver.js +2 -2
  17. package/dist/project.d.ts +5 -2
  18. package/dist/project.js +21 -2
  19. package/dist/service.d.ts +13 -8
  20. package/dist/service.js +1 -1
  21. package/dist/third-service.d.ts +10 -53
  22. package/dist/third-service.js +3 -78
  23. package/dist/typebox-driver.d.ts +0 -6
  24. package/dist/typebox-driver.js +8 -36
  25. package/dist/utils.d.ts +2 -2
  26. package/docs/curd.md +55 -20
  27. package/docs/dao-generation.md +477 -477
  28. package/docs/project.md +32 -24
  29. package/docs/token.md +326 -326
  30. package/package.json +1 -1
  31. package/src/action.ts +51 -51
  32. package/src/controller.ts +53 -53
  33. package/src/convert.ts +76 -78
  34. package/src/curd.ts +104 -104
  35. package/src/dao.ts +504 -485
  36. package/src/dsl.ts +296 -257
  37. package/src/dto.ts +323 -266
  38. package/src/entity.ts +43 -42
  39. package/src/expr.ts +64 -64
  40. package/src/filter.ts +71 -69
  41. package/src/flow-script.ts +702 -695
  42. package/src/flow.ts +1272 -1226
  43. package/src/index.ts +46 -46
  44. package/src/mermaid-driver.ts +339 -339
  45. package/src/mysql-driver.ts +108 -108
  46. package/src/project.ts +138 -114
  47. package/src/service.ts +112 -107
  48. package/src/third-service.ts +68 -191
  49. package/src/typebox-driver.ts +234 -268
  50. package/src/utils.ts +74 -74
@@ -1,269 +1,235 @@
1
- import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef } from './dto.js';
2
- import { EnumField, Field } from './dsl.js';
3
- import type { ThirdMethodSchema } from './third-service.js';
4
-
5
- // TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
6
- // Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
7
- //
8
- // export const RegisterUserInput = Type.Object({...});
9
- // export type RegisterUserInput = Static<typeof RegisterUserInput>;
10
- //
11
- // ENUM fields reference a generated enum (see enum-driver) by its import
12
- // location the resolver maps a jsName to its product import.
13
- // DTO bases (.include()) render as Type.Intersect([...bases, Type.Object({...})]).
14
-
15
- export type EnumResolver = (enumName: string) => ImportBase | undefined;
16
-
17
- function renderString(s: string): string {
18
- return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
19
- }
20
-
21
- function renderDefault(v: unknown): string {
22
- if (typeof v === 'string') return renderString(v);
23
- return JSON.stringify(v);
24
- }
25
-
26
- function renderBasic(
27
- field: Field,
28
- pattern: string | undefined,
29
- defaultValue: unknown,
30
- resolver: EnumResolver | undefined,
31
- indent = 0,
32
- ): string {
33
- if (pattern !== undefined && field.type !== 'string') {
34
- throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
35
- }
36
- const def = defaultValue !== undefined ? `default: ${renderDefault(defaultValue)}` : undefined;
37
- switch (field.type) {
38
- case 'string': {
39
- const opts: string[] = [];
40
- if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
41
- if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
42
- if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
43
- if (def !== undefined) opts.push(def);
44
- return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
45
- }
46
- case 'text':
47
- return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
48
- case 'integer': {
49
- const opts: string[] = [];
50
- if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
51
- if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
52
- if (def !== undefined) opts.push(def);
53
- return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
54
- }
55
- case 'bigint':
56
- case 'decimal':
57
- case 'rate':
58
- case 'time':
59
- case 'date':
60
- case 'datetime':
61
- // Transmitted as string over HTTP: bigint/decimal/rate keep full
62
- // precision, date/time serialize to string.
63
- return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
64
- case 'boolean':
65
- return def !== undefined ? `Type.Boolean({ ${def} })` : 'Type.Boolean()';
66
- case 'json':
67
- return def !== undefined ? `Type.Unknown({ ${def} })` : 'Type.Unknown()';
68
- case 'enum': {
69
- const ref = resolver?.(field.enum.jsName);
70
- if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
71
- if (def !== undefined) {
72
- const member = field.enum.values.find((v) => v.value === defaultValue);
73
- if (!member) {
74
- throw new Error(
75
- `enum field ${field.name}: default ${renderDefault(defaultValue)} is not a member of ${field.enum.jsName}`,
76
- );
77
- }
78
- return `Type.Enum(${ref.name}, { default: ${ref.name}.${member.symbol} })`;
79
- }
80
- return `Type.Enum(${ref.name})`;
81
- }
82
- case 'aggregate':
83
- // Aggregate query outputs: count/sum(int) are numbers, everything else
84
- // arrives as a precision string.
85
- return field.jsType === 'number' ? 'Type.Number()' : 'Type.String()';
86
- case 'array':
87
- return `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
88
- case 'object':
89
- return renderFieldObject(field.properties, indent + 1, resolver);
90
- default:
91
- // Field union is exhaustive; this branch is unreachable at runtime.
92
- throw new Error(`unsupported field type: ${String((field as Field).type)}`);
93
- }
94
- }
95
-
96
- /** Render a plain Field value (wire-format nested fields), wrapping optional. */
97
- function renderFieldValue(field: Field, indent: number, resolver: EnumResolver | undefined): string {
98
- const base = renderBasic(field, undefined, undefined, resolver, indent);
99
- return field.optional ? `Type.Optional(${base})` : base;
100
- }
101
-
102
- /** Render a plain Field object (wire-format nested object). */
103
- function renderFieldObject(properties: Record<string, Field>, indent: number, resolver: EnumResolver | undefined): string {
104
- const pad = ' '.repeat(indent);
105
- const entries = Object.entries(properties).map(([name, f]) => `${pad}${name}: ${renderFieldValue(f, indent, resolver)}`);
106
- return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
107
- }
108
-
109
- function renderObject(fields: Record<string, DtoField | Field>, indent: number, resolver: EnumResolver | undefined): string {
110
- const pad = ' '.repeat(indent);
111
- const entries = Object.entries(fields).map(([name, f]) => {
112
- const rendered = isDtoField(f) ? renderField(f, indent, resolver) : renderFieldValue(f, indent, resolver);
113
- return `${pad}${name}: ${rendered}`;
114
- });
115
- return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
116
- }
117
-
118
- function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
119
- const base = renderValue(f, indent, resolver);
120
- return f.isOptional() ? `Type.Optional(${base})` : base;
121
- }
122
-
123
- function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
124
- if (f.field.type === 'array') {
125
- const items = f.field.items;
126
- // Referenced DTO element — render by name (same-file export), not expanded.
127
- if (isDtoMessage(items)) return `Type.Array(${items.name})`;
128
- if (isDtoField(items)) return `Type.Array(${renderField(items, indent + 1, resolver)})`;
129
- return `Type.Array(${renderFieldValue(items, indent, resolver)})`;
130
- }
131
- if (f.field.type === 'object') {
132
- return renderObject(f.field.properties, indent + 1, resolver);
133
- }
134
- // DtoField only wraps a database Field; array/object defs live in the subclasses.
135
- // Only DTO-level defaults (setDefault) are emitted as TypeBox default
136
- // annotations; DB field defaults are not carried into the API contract.
137
- return renderBasic(f.field as Field, f.pattern, f.default, resolver, indent);
138
- }
139
-
140
- /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
141
- function isDtoMessage(v: unknown): v is DtoMessage {
142
- if (typeof v !== 'object' || v === null) return false;
143
- return (v as Record<string, unknown>).type === 'dto';
144
- }
145
-
146
- /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
147
- function isDtoField(v: unknown): v is DtoField {
148
- if (typeof v !== 'object' || v === null) return false;
149
- return 'field' in v && !('type' in v);
150
- }
151
-
152
- function collectEnumImports(
153
- f: DtoField,
154
- resolver: EnumResolver | undefined,
155
- out: Map<string, ImportBase>,
156
- ): void {
157
- if (f.field.type === 'array') {
158
- const items = f.field.items;
159
- if (isDtoMessage(items)) return;
160
- if (isDtoField(items)) {
161
- collectEnumImports(items, resolver, out);
162
- return;
163
- }
164
- collectFieldEnumImports(items, resolver, out);
165
- return;
166
- }
167
- if (f.field.type === 'object') {
168
- for (const child of Object.values(f.field.properties)) {
169
- if (isDtoField(child)) collectEnumImports(child, resolver, out);
170
- else collectFieldEnumImports(child, resolver, out);
171
- }
172
- return;
173
- }
174
- if (f.field.type === 'enum') collectEnumRef(f.field, resolver, out);
175
- }
176
-
177
- /** Enum import collection over a plain Field (wire-format nested fields). */
178
- function collectFieldEnumImports(
179
- field: Field,
180
- resolver: EnumResolver | undefined,
181
- out: Map<string, ImportBase>,
182
- ): void {
183
- if (field.type === 'array') {
184
- collectFieldEnumImports(field.items, resolver, out);
185
- return;
186
- }
187
- if (field.type === 'object') {
188
- for (const child of Object.values(field.properties)) collectFieldEnumImports(child, resolver, out);
189
- return;
190
- }
191
- if (field.type === 'enum') collectEnumRef(field, resolver, out);
192
- }
193
-
194
- function collectEnumRef(field: EnumField, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void {
195
- const ref = resolver?.(field.enum.jsName);
196
- if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
197
- out.set(`${ref.from}#${ref.name}`, ref);
198
- }
199
-
200
- /** Collect all imports needed to render a DTO: include() bases + enum references. */
201
- export function collectDtoImports(
202
- schema: DtoMessage,
203
- resolver: EnumResolver | undefined,
204
- out: Map<string, ImportBase>,
205
- ): void {
206
- for (const base of schema.bases ?? []) out.set(`${base.from}#${base.name}`, base);
207
- for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, out);
208
- }
209
-
210
- /** Render one DTO export (const + type) no file header, for file-level generation. */
211
- export function renderDtoExport(schema: DtoMessage, resolver: EnumResolver | undefined): string {
212
- const object = renderObject(schema.fields, 1, resolver);
213
- const bases = schema.bases ?? [];
214
- const body =
215
- bases.length > 0
216
- ? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
217
- : object;
218
- return `export const ${schema.name} = ${body};`;
219
- }
220
-
221
- /** Render the Static type export for a DTO. */
222
- export function renderDtoTypeExport(name: string): string {
223
- return `export type ${name} = Static<typeof ${name}>;`;
224
- }
225
-
226
- export function renderDtoMessage(
227
- schema: DtoMessage,
228
- options: { resolver?: EnumResolver; source?: string } = {},
229
- ): string {
230
- const { resolver, source } = options;
231
- const imports = new Map<string, ImportBase>();
232
- collectDtoImports(schema, resolver, imports);
233
-
234
- const header = [
235
- '// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
236
- ...(source !== undefined ? [`// Source: ${source}`] : []),
237
- "import { Type, Static } from '@sinclair/typebox';",
238
- ...[...imports.values()].map((r) => `import${r.type ? ' type' : ''} { ${r.name} } from '${r.from}';`),
239
- ];
240
-
241
- return [
242
- ...header,
243
- '',
244
- renderDtoExport(schema, resolver),
245
- renderDtoTypeExport(schema.name),
246
- '',
247
- ].join('\n');
248
- }
249
-
250
- function renderBase(base: ImportRef): string {
251
- if (base.args === undefined || base.args.length === 0) return base.name;
252
- const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
253
- return `${base.name}(${args.join(', ')})`;
254
- }
255
-
256
- /** Render one third-party method message export (const only — pair with
257
- * renderDtoTypeExport for the Static type). */
258
- export function renderThirdMethodExport(schema: ThirdMethodSchema, resolver: EnumResolver | undefined): string {
259
- return `export const ${schema.name} = ${renderFieldObject(schema.fields, 1, resolver)};`;
260
- }
261
-
262
- /** Collect all imports needed to render a third-party method message: enum references. */
263
- export function collectThirdMethodImports(
264
- schema: ThirdMethodSchema,
265
- resolver: EnumResolver | undefined,
266
- out: Map<string, ImportBase>,
267
- ): void {
268
- for (const f of Object.values(schema.fields)) collectFieldEnumImports(f, resolver, out);
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
+ function renderBasic(
26
+ field: Field,
27
+ pattern: string | undefined,
28
+ defaultValue: unknown,
29
+ resolver: EnumResolver | undefined,
30
+ indent = 0,
31
+ ): string {
32
+ if (pattern !== undefined && field.type !== 'string') {
33
+ throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
34
+ }
35
+ const def = defaultValue !== undefined ? `default: ${renderDefault(defaultValue)}` : undefined;
36
+ switch (field.type) {
37
+ case 'string': {
38
+ const opts: string[] = [];
39
+ if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
40
+ if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
41
+ if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
42
+ if (def !== undefined) opts.push(def);
43
+ return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
44
+ }
45
+ case 'text':
46
+ return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
47
+ case 'integer': {
48
+ const opts: string[] = [];
49
+ if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
50
+ if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
51
+ if (def !== undefined) opts.push(def);
52
+ return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
53
+ }
54
+ case 'bigint':
55
+ case 'decimal':
56
+ case 'rate':
57
+ case 'time':
58
+ case 'date':
59
+ case 'datetime':
60
+ // Transmitted as string over HTTP: bigint/decimal/rate keep full
61
+ // precision, date/time serialize to string.
62
+ return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
63
+ case 'boolean':
64
+ return def !== undefined ? `Type.Boolean({ ${def} })` : 'Type.Boolean()';
65
+ case 'json':
66
+ return def !== undefined ? `Type.Unknown({ ${def} })` : 'Type.Unknown()';
67
+ case 'enum': {
68
+ const ref = resolver?.(field.enum.jsName);
69
+ if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
70
+ if (def !== undefined) {
71
+ const member = field.enum.values.find((v) => v.value === defaultValue);
72
+ if (!member) {
73
+ throw new Error(
74
+ `enum field ${field.name}: default ${renderDefault(defaultValue)} is not a member of ${field.enum.jsName}`,
75
+ );
76
+ }
77
+ return `Type.Enum(${ref.name}, { default: ${ref.name}.${member.symbol} })`;
78
+ }
79
+ return `Type.Enum(${ref.name})`;
80
+ }
81
+ case 'aggregate':
82
+ // Aggregate query outputs: count/sum(int) are numbers, everything else
83
+ // arrives as a precision string.
84
+ return field.jsType === 'number' ? 'Type.Number()' : 'Type.String()';
85
+ case 'array':
86
+ return `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
87
+ case 'object':
88
+ return renderFieldObject(field.properties, indent + 1, resolver);
89
+ default:
90
+ // Field union is exhaustive; this branch is unreachable at runtime.
91
+ throw new Error(`unsupported field type: ${String((field as Field).type)}`);
92
+ }
93
+ }
94
+
95
+ /** Render a plain Field value (wire-format nested fields), wrapping optional. */
96
+ function renderFieldValue(field: Field, indent: number, resolver: EnumResolver | undefined): string {
97
+ const base = renderBasic(field, undefined, undefined, resolver, indent);
98
+ return field.optional ? `Type.Optional(${base})` : base;
99
+ }
100
+
101
+ /** Render a plain Field object (wire-format nested object). */
102
+ function renderFieldObject(properties: Record<string, Field>, indent: number, resolver: EnumResolver | undefined): string {
103
+ const pad = ' '.repeat(indent);
104
+ const entries = Object.entries(properties).map(([name, f]) => `${pad}${name}: ${renderFieldValue(f, indent, resolver)}`);
105
+ return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
106
+ }
107
+
108
+ function renderObject(fields: Record<string, DtoField | Field>, indent: number, resolver: EnumResolver | undefined): string {
109
+ const pad = ' '.repeat(indent);
110
+ const entries = Object.entries(fields).map(([name, f]) => {
111
+ const rendered = isDtoField(f) ? renderField(f, indent, resolver) : renderFieldValue(f, indent, resolver);
112
+ return `${pad}${name}: ${rendered}`;
113
+ });
114
+ return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
115
+ }
116
+
117
+ function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
118
+ const base = renderValue(f, indent, resolver);
119
+ return f.isOptional() ? `Type.Optional(${base})` : base;
120
+ }
121
+
122
+ function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
123
+ if (f.field.type === 'array') {
124
+ const items = f.field.items;
125
+ // Referenced DTO element — render by name (same-file export), not expanded.
126
+ if (isDtoMessage(items)) return `Type.Array(${items.name})`;
127
+ if (isDtoField(items)) return `Type.Array(${renderField(items, indent + 1, resolver)})`;
128
+ return `Type.Array(${renderFieldValue(items, indent, resolver)})`;
129
+ }
130
+ if (f.field.type === 'object') {
131
+ return renderObject(f.field.properties, indent + 1, resolver);
132
+ }
133
+ // DtoField only wraps a database Field; array/object defs live in the subclasses.
134
+ // Only DTO-level defaults (setDefault) are emitted as TypeBox default
135
+ // annotations; DB field defaults are not carried into the API contract.
136
+ return renderBasic(f.field as Field, f.pattern, f.default, resolver, indent);
137
+ }
138
+
139
+ function collectEnumImports(
140
+ f: DtoField,
141
+ resolver: EnumResolver | undefined,
142
+ out: Map<string, ImportBase>,
143
+ ): void {
144
+ if (f.field.type === 'array') {
145
+ const items = f.field.items;
146
+ if (isDtoMessage(items)) return;
147
+ if (isDtoField(items)) {
148
+ collectEnumImports(items, resolver, out);
149
+ return;
150
+ }
151
+ collectFieldEnumImports(items, resolver, out);
152
+ return;
153
+ }
154
+ if (f.field.type === 'object') {
155
+ for (const child of Object.values(f.field.properties)) {
156
+ if (isDtoField(child)) collectEnumImports(child, resolver, out);
157
+ else collectFieldEnumImports(child, resolver, out);
158
+ }
159
+ return;
160
+ }
161
+ if (f.field.type === 'enum') collectEnumRef(f.field, resolver, out);
162
+ }
163
+
164
+ /** Enum import collection over a plain Field (wire-format nested fields). */
165
+ function collectFieldEnumImports(
166
+ field: Field,
167
+ resolver: EnumResolver | undefined,
168
+ out: Map<string, ImportBase>,
169
+ ): void {
170
+ for (const jsName of collectEnumRefs(field)) {
171
+ const ref = resolver?.(jsName);
172
+ if (!ref) throw new Error(`enum ${jsName}: no import ref — pass an EnumResolver`);
173
+ out.set(`${ref.from}#${ref.name}`, ref);
174
+ }
175
+ }
176
+
177
+ function collectEnumRef(field: EnumField, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void {
178
+ collectFieldEnumImports(field, resolver, out);
179
+ }
180
+
181
+ /** Collect all imports needed to render a DTO: include() bases + enum references. */
182
+ export function collectDtoImports(
183
+ schema: DtoMessage,
184
+ resolver: EnumResolver | undefined,
185
+ out: Map<string, ImportBase>,
186
+ ): void {
187
+ for (const base of schema.bases ?? []) out.set(`${base.from}#${base.name}`, base);
188
+ for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, out);
189
+ }
190
+
191
+ /** Render one DTO export (const + type) no file header, for file-level generation. */
192
+ export function renderDtoExport(schema: DtoMessage, resolver: EnumResolver | undefined): string {
193
+ const object = renderObject(schema.fields, 1, resolver);
194
+ const bases = schema.bases ?? [];
195
+ const body =
196
+ bases.length > 0
197
+ ? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
198
+ : object;
199
+ return `export const ${schema.name} = ${body};`;
200
+ }
201
+
202
+ /** Render the Static type export for a DTO. */
203
+ export function renderDtoTypeExport(name: string): string {
204
+ return `export type ${name} = Static<typeof ${name}>;`;
205
+ }
206
+
207
+ export function renderDtoMessage(
208
+ schema: DtoMessage,
209
+ options: { resolver?: EnumResolver; source?: string } = {},
210
+ ): string {
211
+ const { resolver, source } = options;
212
+ const imports = new Map<string, ImportBase>();
213
+ collectDtoImports(schema, resolver, imports);
214
+
215
+ const header = [
216
+ '// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
217
+ ...(source !== undefined ? [`// Source: ${source}`] : []),
218
+ "import { Type, Static } from '@sinclair/typebox';",
219
+ ...[...imports.values()].map((r) => `import${r.type ? ' type' : ''} { ${r.name} } from '${r.from}';`),
220
+ ];
221
+
222
+ return [
223
+ ...header,
224
+ '',
225
+ renderDtoExport(schema, resolver),
226
+ renderDtoTypeExport(schema.name),
227
+ '',
228
+ ].join('\n');
229
+ }
230
+
231
+ function renderBase(base: ImportRef): string {
232
+ if (base.args === undefined || base.args.length === 0) return base.name;
233
+ const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
234
+ return `${base.name}(${args.join(', ')})`;
269
235
  }