@pylonts/dsl 1.1.13 → 1.1.14
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/dto.d.ts +4 -0
- package/dist/dto.js +7 -0
- package/package.json +1 -1
- package/src/dto.ts +331 -323
package/dist/dto.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare class DtoField implements SchemaBase {
|
|
|
47
47
|
operator?: Operator;
|
|
48
48
|
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
49
49
|
default?: unknown;
|
|
50
|
+
/** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
|
|
51
|
+
ref?: DtoField;
|
|
50
52
|
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef);
|
|
51
53
|
setPattern(value: string): this;
|
|
52
54
|
setDescription(value: string): this;
|
|
@@ -56,6 +58,8 @@ export declare class DtoField implements SchemaBase {
|
|
|
56
58
|
setOptional(value: boolean): this;
|
|
57
59
|
/** Set a default value — emitted as a TypeBox schema default annotation */
|
|
58
60
|
setDefault(value: unknown): this;
|
|
61
|
+
/** Reference another DtoField — this field reuses the referenced field's type/constraints */
|
|
62
|
+
setRef(value: DtoField): this;
|
|
59
63
|
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
60
64
|
setOperator(value: Operator): this;
|
|
61
65
|
/** optional 优先于 field.optional */
|
package/dist/dto.js
CHANGED
|
@@ -14,6 +14,8 @@ export class DtoField {
|
|
|
14
14
|
operator;
|
|
15
15
|
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
16
16
|
default;
|
|
17
|
+
/** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
|
|
18
|
+
ref;
|
|
17
19
|
constructor(field) {
|
|
18
20
|
this.name = '';
|
|
19
21
|
this.field = field;
|
|
@@ -42,6 +44,11 @@ export class DtoField {
|
|
|
42
44
|
this.default = value;
|
|
43
45
|
return this;
|
|
44
46
|
}
|
|
47
|
+
/** Reference another DtoField — this field reuses the referenced field's type/constraints */
|
|
48
|
+
setRef(value) {
|
|
49
|
+
this.ref = value;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
45
52
|
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
46
53
|
setOperator(value) {
|
|
47
54
|
this.operator = value;
|
package/package.json
CHANGED
package/src/dto.ts
CHANGED
|
@@ -1,324 +1,332 @@
|
|
|
1
|
-
import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase, walkContainer } from './dsl.js';
|
|
2
|
-
import { TableSchema } from './db.js';
|
|
3
|
-
import type { EntitySchema } from './entity.js';
|
|
4
|
-
import type { ImportBase } from './import-base.js';
|
|
5
|
-
import { toCamelCase } from '@pylonts/core';
|
|
6
|
-
|
|
7
|
-
// Interface (DTO) field definitions.
|
|
8
|
-
// Naming convention: all DTO types and builders use the Dto prefix.
|
|
9
|
-
// A DtoField stores the database Field and the API-only extras separately.
|
|
10
|
-
|
|
11
|
-
/** Re-export — Operator lives on the DSL level (see dsl.ts). */
|
|
12
|
-
export type { Operator } from './dsl.js';
|
|
13
|
-
|
|
14
|
-
/** Re-export — ImportBase lives on its own module (see import-base.ts). */
|
|
15
|
-
export type { ImportBase } from './import-base.js';
|
|
16
|
-
|
|
17
|
-
/** Re-export — MockDescriptor lives on its own module (see mock.ts). */
|
|
18
|
-
export type { MockDescriptor } from './mock.js';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Reference to an existing TypeBox base schema by its import location.
|
|
22
|
-
* Serializable metadata: the driver renders `import { name } from 'from'`
|
|
23
|
-
* and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
|
|
24
|
-
*/
|
|
25
|
-
export interface ImportRef extends ImportBase {
|
|
26
|
-
/**
|
|
27
|
-
* Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
|
|
28
|
-
* Two forms, both local DTOs:
|
|
29
|
-
* string — the DTO export name
|
|
30
|
-
* DtoMessage — the DTO instance itself; the driver resolves it to its name
|
|
31
|
-
*/
|
|
32
|
-
args?: (string | DtoMessage)[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type DtoArrayFieldDef = BaseField & {
|
|
36
|
-
type: 'array';
|
|
37
|
-
jsType: 'array';
|
|
38
|
-
/** Element type: inline DtoField, or a reference to an existing DTO (rendered by name) */
|
|
39
|
-
items: DtoField | DtoMessage;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type DtoObjectFieldDef = BaseField & {
|
|
43
|
-
type: 'object';
|
|
44
|
-
jsType: 'object';
|
|
45
|
-
properties: Record<string, DtoField>;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export class DtoField implements SchemaBase {
|
|
49
|
-
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
50
|
-
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
51
|
-
name: string;
|
|
52
|
-
/** 字段描述 */
|
|
53
|
-
description?: string;
|
|
54
|
-
/** 所属容器(buildMessage / defineRouteData / definePageData 反写) */
|
|
55
|
-
schema?: CollectionSchemaBase;
|
|
56
|
-
field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
|
|
57
|
-
pattern?: string;
|
|
58
|
-
optional?: boolean;
|
|
59
|
-
operator?: Operator;
|
|
60
|
-
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
61
|
-
default?: unknown;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
return this.field.
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export
|
|
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
|
-
this.
|
|
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
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
df
|
|
199
|
-
df
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1
|
+
import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase, walkContainer } from './dsl.js';
|
|
2
|
+
import { TableSchema } from './db.js';
|
|
3
|
+
import type { EntitySchema } from './entity.js';
|
|
4
|
+
import type { ImportBase } from './import-base.js';
|
|
5
|
+
import { toCamelCase } from '@pylonts/core';
|
|
6
|
+
|
|
7
|
+
// Interface (DTO) field definitions.
|
|
8
|
+
// Naming convention: all DTO types and builders use the Dto prefix.
|
|
9
|
+
// A DtoField stores the database Field and the API-only extras separately.
|
|
10
|
+
|
|
11
|
+
/** Re-export — Operator lives on the DSL level (see dsl.ts). */
|
|
12
|
+
export type { Operator } from './dsl.js';
|
|
13
|
+
|
|
14
|
+
/** Re-export — ImportBase lives on its own module (see import-base.ts). */
|
|
15
|
+
export type { ImportBase } from './import-base.js';
|
|
16
|
+
|
|
17
|
+
/** Re-export — MockDescriptor lives on its own module (see mock.ts). */
|
|
18
|
+
export type { MockDescriptor } from './mock.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reference to an existing TypeBox base schema by its import location.
|
|
22
|
+
* Serializable metadata: the driver renders `import { name } from 'from'`
|
|
23
|
+
* and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
|
|
24
|
+
*/
|
|
25
|
+
export interface ImportRef extends ImportBase {
|
|
26
|
+
/**
|
|
27
|
+
* Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
|
|
28
|
+
* Two forms, both local DTOs:
|
|
29
|
+
* string — the DTO export name
|
|
30
|
+
* DtoMessage — the DTO instance itself; the driver resolves it to its name
|
|
31
|
+
*/
|
|
32
|
+
args?: (string | DtoMessage)[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DtoArrayFieldDef = BaseField & {
|
|
36
|
+
type: 'array';
|
|
37
|
+
jsType: 'array';
|
|
38
|
+
/** Element type: inline DtoField, or a reference to an existing DTO (rendered by name) */
|
|
39
|
+
items: DtoField | DtoMessage;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type DtoObjectFieldDef = BaseField & {
|
|
43
|
+
type: 'object';
|
|
44
|
+
jsType: 'object';
|
|
45
|
+
properties: Record<string, DtoField>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class DtoField implements SchemaBase {
|
|
49
|
+
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
50
|
+
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
51
|
+
name: string;
|
|
52
|
+
/** 字段描述 */
|
|
53
|
+
description?: string;
|
|
54
|
+
/** 所属容器(buildMessage / defineRouteData / definePageData 反写) */
|
|
55
|
+
schema?: CollectionSchemaBase;
|
|
56
|
+
field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
|
|
57
|
+
pattern?: string;
|
|
58
|
+
optional?: boolean;
|
|
59
|
+
operator?: Operator;
|
|
60
|
+
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
61
|
+
default?: unknown;
|
|
62
|
+
/** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
|
|
63
|
+
ref?: DtoField;
|
|
64
|
+
|
|
65
|
+
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
|
|
66
|
+
this.name = '';
|
|
67
|
+
this.field = field;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setPattern(value: string): this {
|
|
71
|
+
this.pattern = value;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setDescription(value: string): this {
|
|
76
|
+
this.description = value;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getDescription(): string | undefined {
|
|
81
|
+
return this.description;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True when this field wraps a DB column (picked via from()); false for inline fields. */
|
|
85
|
+
isColumn(): boolean {
|
|
86
|
+
return this.field.schema?.type === 'table';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setOptional(value: boolean): this {
|
|
90
|
+
this.optional = value;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Set a default value — emitted as a TypeBox schema default annotation */
|
|
95
|
+
setDefault(value: unknown): this {
|
|
96
|
+
this.default = value;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Reference another DtoField — this field reuses the referenced field's type/constraints */
|
|
101
|
+
setRef(value: DtoField): this {
|
|
102
|
+
this.ref = value;
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
107
|
+
setOperator(value: Operator): this {
|
|
108
|
+
this.operator = value;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** optional 优先于 field.optional */
|
|
113
|
+
isOptional(): boolean {
|
|
114
|
+
if (this.optional !== undefined) return this.optional;
|
|
115
|
+
return this.field.optional ?? false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class DtoArrayField extends DtoField {
|
|
120
|
+
declare field: DtoArrayFieldDef;
|
|
121
|
+
|
|
122
|
+
/** Element type: inline DtoField or a referenced DtoMessage (rendered by name). */
|
|
123
|
+
items(): DtoField | DtoMessage {
|
|
124
|
+
return this.field.items;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class DtoObjectField extends DtoField {
|
|
129
|
+
declare field: DtoObjectFieldDef;
|
|
130
|
+
|
|
131
|
+
properties(): Record<string, DtoField> {
|
|
132
|
+
return this.field.properties;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export enum DtoDirection {
|
|
137
|
+
Input = 'input',
|
|
138
|
+
Output = 'output',
|
|
139
|
+
Query = 'query',
|
|
140
|
+
Pk = 'pk',
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class DtoMessage implements CollectionSchemaBase {
|
|
144
|
+
type = 'dto';
|
|
145
|
+
name: string;
|
|
146
|
+
description?: string;
|
|
147
|
+
/** 方向:输入或输出 */
|
|
148
|
+
direction: DtoDirection;
|
|
149
|
+
fields: Record<string, DtoField>;
|
|
150
|
+
/** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
|
|
151
|
+
bases: ImportRef[] = [];
|
|
152
|
+
|
|
153
|
+
constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
|
|
154
|
+
this.name = name;
|
|
155
|
+
this.direction = direction;
|
|
156
|
+
this.fields = fields;
|
|
157
|
+
this.description = description;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
|
|
161
|
+
include(...refs: ImportRef[]): this {
|
|
162
|
+
this.bases.push(...refs);
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function dtoField(field: Field | DtoArrayFieldDef | DtoObjectFieldDef): DtoField {
|
|
168
|
+
return new DtoField(field);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
|
|
172
|
+
export function isDtoMessage(v: unknown): v is DtoMessage {
|
|
173
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
174
|
+
return (v as Record<string, unknown>).type === 'dto';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
|
|
178
|
+
export function isDtoField(v: unknown): v is DtoField {
|
|
179
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
180
|
+
return 'field' in v && !('type' in v);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
|
|
184
|
+
// Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
|
|
185
|
+
// referenced by name (the driver renders Type.Array(<DtoName>)).
|
|
186
|
+
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
|
|
190
|
+
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
194
|
+
const message = new DtoMessage(name, direction, fields, description);
|
|
195
|
+
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
196
|
+
// are created per DTO, never shared).
|
|
197
|
+
for (const key of Object.keys(message.fields)) {
|
|
198
|
+
const df = message.fields[key];
|
|
199
|
+
if (!(df instanceof DtoField)) {
|
|
200
|
+
const got = df === null || df === undefined ? String(df) : `${(df as object).constructor.name ?? typeof df}`;
|
|
201
|
+
throw new Error(
|
|
202
|
+
`[dto] DTO "${name}" field "${key}" must be a DtoField (created with dtoField()/dtoArrayField()/dtoObjectField()), got ${got}. ` +
|
|
203
|
+
`Did you pass enumField(...) directly? Use dtoField(enumField({...})) instead.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
df.name = key;
|
|
207
|
+
df.schema = message;
|
|
208
|
+
// Custom (inline) fields are owned by this DTO: write back name + schema.
|
|
209
|
+
// Fields picked via from() share the database Field instance whose
|
|
210
|
+
// name/schema already point to the table — leave them untouched.
|
|
211
|
+
if (df.field.schema === undefined) {
|
|
212
|
+
df.field.name = key;
|
|
213
|
+
df.field.schema = message;
|
|
214
|
+
}
|
|
215
|
+
writeBackNested(df, message);
|
|
216
|
+
}
|
|
217
|
+
return message;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Write back name/schema on nested DTO fields (array items, object
|
|
221
|
+
* properties) — both plain-Field containers (objectField/arrayField) and
|
|
222
|
+
* DtoField containers (dtoObjectField/dtoArrayField). DtoMessage item
|
|
223
|
+
* references are skipped — they carry their own identity. */
|
|
224
|
+
function writeBackNested(df: DtoField, message: DtoMessage): void {
|
|
225
|
+
const f = df.field;
|
|
226
|
+
if (f.type === 'array') {
|
|
227
|
+
const items = f.items;
|
|
228
|
+
if (isDtoMessage(items)) return;
|
|
229
|
+
if (isDtoField(items)) {
|
|
230
|
+
writeBackNested(items, message);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
walkContainer(items, writeBackLeaf(message));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (f.type === 'object') {
|
|
237
|
+
for (const [key, child] of Object.entries(f.properties)) {
|
|
238
|
+
if (isDtoField(child)) {
|
|
239
|
+
child.name = key;
|
|
240
|
+
child.schema = message;
|
|
241
|
+
if (child.field.schema === undefined) {
|
|
242
|
+
child.field.name = key;
|
|
243
|
+
child.field.schema = message;
|
|
244
|
+
}
|
|
245
|
+
writeBackNested(child, message);
|
|
246
|
+
} else {
|
|
247
|
+
writeBackLeaf(message)(child, key);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Name/schema write-back for a plain Field (own fields only — shared
|
|
254
|
+
* instances keep their original identity). */
|
|
255
|
+
function writeBackLeaf(message: DtoMessage): (f: Field, key?: string) => void {
|
|
256
|
+
return (f, key) => {
|
|
257
|
+
if (key !== undefined && f.schema === undefined) {
|
|
258
|
+
f.name = key;
|
|
259
|
+
f.schema = message;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
265
|
+
const message = buildMessage(name, DtoDirection.Input, fields, description);
|
|
266
|
+
// Rule A — set optionality from the DB column rule (skips fields the author
|
|
267
|
+
// already set): nullable / default → optional, else required.
|
|
268
|
+
// PK columns are always required.
|
|
269
|
+
for (const field of Object.values(message.fields)) {
|
|
270
|
+
if (field.optional !== undefined) continue;
|
|
271
|
+
const f = field.field as Field;
|
|
272
|
+
if (f.schema?.type !== 'table') continue;
|
|
273
|
+
const table = f.schema as TableSchema;
|
|
274
|
+
field.optional = table.isPk(f) ? false : f.optional !== false || f.default !== undefined;
|
|
275
|
+
}
|
|
276
|
+
return message;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
280
|
+
return buildMessage(name, DtoDirection.Output, fields, description);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
284
|
+
const message = buildMessage(name, DtoDirection.Query, fields, description);
|
|
285
|
+
// Rule B: query/search fields are always optional.
|
|
286
|
+
for (const field of Object.values(message.fields)) field.optional = true;
|
|
287
|
+
return message;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
291
|
+
const message = buildMessage(name, DtoDirection.Pk, fields, description);
|
|
292
|
+
// Rule P: PK locator fields are required, other fields are optional.
|
|
293
|
+
for (const field of Object.values(message.fields)) {
|
|
294
|
+
if (field.optional !== undefined) continue;
|
|
295
|
+
const f = field.field as Field;
|
|
296
|
+
const table = f.schema?.type === 'table' ? (f.schema as TableSchema) : undefined;
|
|
297
|
+
field.optional = table !== undefined && table.isPk(f) ? false : true;
|
|
298
|
+
}
|
|
299
|
+
return message;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Field-collection source a DTO can project from: a DB table, another DTO
|
|
303
|
+
* message (protocol fields keep their names), or an entity (which may carry
|
|
304
|
+
* aggregate fields). */
|
|
305
|
+
export type DtoFieldSource = TableSchema | DtoMessage | EntitySchema;
|
|
306
|
+
|
|
307
|
+
function ownsField(source: DtoFieldSource, field: Field | DtoArrayFieldDef | DtoObjectFieldDef): boolean {
|
|
308
|
+
if (isDtoMessage(source)) {
|
|
309
|
+
return Object.values(source.fields).some((df) => df.field === field);
|
|
310
|
+
}
|
|
311
|
+
return Object.values(source.columns).some((c) => c === field);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Project fields from a field-collection source (table, DTO message or
|
|
315
|
+
* entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
|
|
316
|
+
* instances keep their original identity — the projection references them. */
|
|
317
|
+
export function from(
|
|
318
|
+
source: DtoFieldSource,
|
|
319
|
+
fields: (Field | DtoArrayFieldDef | DtoObjectFieldDef)[],
|
|
320
|
+
): Record<string, DtoField> {
|
|
321
|
+
const out: Record<string, DtoField> = {};
|
|
322
|
+
for (const field of fields) {
|
|
323
|
+
if (!ownsField(source, field)) {
|
|
324
|
+
throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${isDtoMessage(source) ? 'dto' : source.type}`);
|
|
325
|
+
}
|
|
326
|
+
// DB columns map to camelCase interface names (mer_id → merId); aggregate
|
|
327
|
+
// field names are already camel and pass through; DTO message fields are
|
|
328
|
+
// protocol names themselves and stay untouched.
|
|
329
|
+
out[isDtoMessage(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
324
332
|
}
|