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