@pylonts/dsl 1.1.5 → 1.1.6

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 (62) hide show
  1. package/dist/controller.d.ts +27 -0
  2. package/dist/controller.js +11 -0
  3. package/dist/convert.d.ts +18 -6
  4. package/dist/convert.js +12 -2
  5. package/dist/curd.d.ts +0 -2
  6. package/dist/curd.js +7 -0
  7. package/dist/dao.d.ts +111 -2
  8. package/dist/dao.js +28 -2
  9. package/dist/db.js +3 -0
  10. package/dist/dsl.d.ts +16 -1
  11. package/dist/dsl.js +6 -0
  12. package/dist/dto.d.ts +6 -2
  13. package/dist/dto.js +20 -9
  14. package/dist/endpoint.d.ts +15 -0
  15. package/dist/endpoint.js +3 -0
  16. package/dist/exception.d.ts +14 -0
  17. package/dist/exception.js +9 -0
  18. package/dist/field-rule.d.ts +20 -0
  19. package/dist/field-rule.js +19 -0
  20. package/dist/flow.d.ts +24 -2
  21. package/dist/flow.js +16 -4
  22. package/dist/index.d.ts +7 -0
  23. package/dist/index.js +9 -0
  24. package/dist/mermaid-driver.js +32 -3
  25. package/dist/method.d.ts +11 -0
  26. package/dist/method.js +3 -0
  27. package/dist/mysql-driver.js +4 -0
  28. package/dist/provider.d.ts +6 -11
  29. package/dist/provider.js +2 -2
  30. package/dist/service.d.ts +16 -6
  31. package/dist/service.js +13 -2
  32. package/dist/third-service.d.ts +75 -0
  33. package/dist/third-service.js +96 -0
  34. package/dist/typebox-driver.d.ts +6 -0
  35. package/dist/typebox-driver.js +73 -12
  36. package/dist/utils.d.ts +25 -10
  37. package/dist/utils.js +28 -11
  38. package/docs/dto.md +73 -66
  39. package/docs/third-service.md +122 -0
  40. package/package.json +4 -1
  41. package/src/controller.ts +40 -0
  42. package/src/convert.ts +42 -15
  43. package/src/curd.ts +98 -93
  44. package/src/dao.ts +172 -13
  45. package/src/db.ts +186 -181
  46. package/src/dsl.ts +26 -1
  47. package/src/dto.ts +263 -247
  48. package/src/endpoint.ts +18 -0
  49. package/src/exception.ts +28 -0
  50. package/src/field-rule.ts +47 -0
  51. package/src/flow.ts +143 -103
  52. package/src/index.ts +43 -33
  53. package/src/mermaid-driver.ts +112 -84
  54. package/src/method.ts +20 -0
  55. package/src/mysql-driver.ts +4 -0
  56. package/src/provider.ts +67 -72
  57. package/src/service.ts +42 -20
  58. package/src/third-service.ts +186 -0
  59. package/src/typebox-driver.ts +82 -11
  60. package/src/utils.ts +63 -26
  61. package/dist/check-inheritance.d.ts +0 -9
  62. package/dist/check-inheritance.js +0 -58
package/src/provider.ts CHANGED
@@ -1,73 +1,68 @@
1
- import type { ImportBase } from './import-base.js';
2
- import type { ImportableSchemaBase } from './dsl.js';
3
- import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
4
- import type { ActionSchema } from './action.js';
5
- import type { RefSchema } from './ref.js';
6
- import type { ConvertSchema } from './convert.js';
7
-
8
- // ProviderSchema: an API function signature (args DTO + results DTO).
9
-
10
- /** Parameter data source for a call argument. */
11
- export type DataRef =
12
- | { type: 'route'; key: string }
13
- | { type: 'data'; key: string }
14
- | { type: 'value'; value: unknown };
15
-
16
- /** Create a route-parameter reference. */
17
- export function route(key: string): DataRef {
18
- return { type: 'route', key };
19
- }
20
-
21
- /** Create a page-data reference. */
22
- export function data(key: string): DataRef {
23
- return { type: 'data', key };
24
- }
25
-
26
- export interface CallAction extends ActionSchema {
27
- type: 'call';
28
- func: ProviderSchema;
29
- args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
30
- }
31
-
32
-
33
- export function call(func: ProviderSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction {
34
- return { name: func.name, type: 'call', func, args };
35
- }
36
-
37
- /** Provider function signature. The generated API client exposes one function
38
- * per endpoint; ProviderSchema gives that function a name and types. */
39
- export interface ProviderSchema extends ImportableSchemaBase {
40
- isAsync: boolean;
41
- /** Input DTO (buildInput / buildQuery / buildPk result). */
42
- args: DtoMessage;
43
- /** Output DTO (buildOutput result) or primitive. */
44
- results: DtoMessage | number | boolean | string;
45
- }
46
-
47
- /** Define a provider function. */
48
- export function defineProvider(
49
- name: string,
50
- schema: {
51
- isAsync: boolean;
52
- args: DtoMessage;
53
- results: DtoMessage | number | boolean | string;
54
- description?: string;
55
- importRef?: ImportBase;
56
- },
57
- ): ProviderSchema {
58
- return { name, ...schema };
59
- }
60
-
61
- /** Assign a call's result to a page data field.
62
- * React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
63
- export interface SetDataAction extends ActionSchema {
64
- type: 'setData';
65
- call: CallAction;
66
- field: DtoField;
67
- /** Optional field-level transform before assignment. */
68
- convert?: ConvertSchema;
69
- }
70
-
71
- export function setData(call: CallAction, field: DtoField, convert?: ConvertSchema): SetDataAction {
72
- return { name: 'setData', type: 'setData', call, field, convert };
1
+ import type { ImportBase } from './import-base.js';
2
+ import type { ImportableSchemaBase } from './dsl.js';
3
+ import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
4
+ import type { ActionSchema } from './action.js';
5
+ import type { RefSchema } from './ref.js';
6
+ import type { EndpointSchema } from './endpoint.js';
7
+
8
+ // ProviderSchema: an API function signature (args DTO + results DTO).
9
+
10
+ /** Parameter data source for a call argument. */
11
+ export type DataRef =
12
+ | { type: 'route'; key: string }
13
+ | { type: 'data'; key: string }
14
+ | { type: 'value'; value: unknown };
15
+
16
+ /** Create a route-parameter reference. */
17
+ export function route(key: string): DataRef {
18
+ return { type: 'route', key };
19
+ }
20
+
21
+ /** Create a page-data reference. */
22
+ export function data(key: string): DataRef {
23
+ return { type: 'data', key };
24
+ }
25
+
26
+ export interface CallAction extends ActionSchema {
27
+ type: 'call';
28
+ func: ProviderSchema;
29
+ args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
30
+ }
31
+
32
+
33
+ export function call(func: ProviderSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction {
34
+ return { name: func.name, type: 'call', func, args };
35
+ }
36
+
37
+ /** Provider function signature. The generated API client exposes one function
38
+ * per endpoint; ProviderSchema gives that function a name and a shared signature. */
39
+ export interface ProviderSchema extends ImportableSchemaBase {
40
+ isAsync: boolean;
41
+ /** Shared API signature same instance the backend controller method references. */
42
+ signature: EndpointSchema;
43
+ }
44
+
45
+ /** Define a provider function. */
46
+ export function defineProvider(
47
+ name: string,
48
+ schema: {
49
+ isAsync: boolean;
50
+ signature: EndpointSchema;
51
+ description?: string;
52
+ importRef?: ImportBase;
53
+ },
54
+ ): ProviderSchema {
55
+ return { name, ...schema };
56
+ }
57
+
58
+ /** Assign a call's result to a page data field.
59
+ * React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
60
+ export interface SetDataAction extends ActionSchema {
61
+ type: 'setData';
62
+ call: CallAction;
63
+ field: DtoField;
64
+ }
65
+
66
+ export function setData(call: CallAction, field: DtoField): SetDataAction {
67
+ return { name: 'setData', type: 'setData', call, field };
73
68
  }
package/src/service.ts CHANGED
@@ -1,21 +1,43 @@
1
- import { SchemaBase } from './dsl.js';
2
- import { FrontAppSchema } from './project.js';
3
- import type { DtoArrayField, DtoField, DtoMessage, DtoObjectField } from './dto.js';
4
-
5
- /** A frontend service bound to exactly one frontend app. */
6
- export interface ServiceSchema extends SchemaBase {
7
- type: 'service';
8
- /** The frontend app this service belongs to (shared instance from project.config). */
9
- app: FrontAppSchema;
10
- }
11
-
12
- export function defineService(name: string, app: FrontAppSchema, description?: string): ServiceSchema {
13
- return { name, type: 'service', app, description };
14
- }
15
-
16
- /** A method exposed by a service. */
17
- export interface ServiceMethodSchema extends SchemaBase {
18
- type: 'method';
19
- name: string;
20
- fields: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField>;
1
+ import { SchemaBase } from './dsl.js';
2
+ import { FrontAppSchema } from './project.js';
3
+ import type { DtoArrayField, DtoField, DtoMessage, DtoObjectField } from './dto.js';
4
+
5
+ /** A backend service serving exactly one frontend app (1:1 module). */
6
+ export interface ServiceSchema extends SchemaBase {
7
+ type: 'service';
8
+ /** The frontend app this service serves (shared instance from project.config). */
9
+ app: FrontAppSchema;
10
+ /** Methods keyed by name — the map key is written back as the method name. */
11
+ methods: Record<string, ServiceMethodSchema>;
12
+ }
13
+
14
+ /** Method input for defineService: type/schema/name are set by the builder. */
15
+ export type ServiceMethodDef = Omit<ServiceMethodSchema, 'type' | 'schema' | 'name'>;
16
+
17
+ export function defineService(options: {
18
+ name: string;
19
+ app: FrontAppSchema;
20
+ methods: Record<string, ServiceMethodDef>;
21
+ description?: string;
22
+ }): ServiceSchema {
23
+ const schema: ServiceSchema = {
24
+ type: 'service',
25
+ name: options.name,
26
+ description: options.description,
27
+ app: options.app,
28
+ methods: {},
29
+ };
30
+ for (const key of Object.keys(options.methods)) {
31
+ const method = options.methods[key] as ServiceMethodDef;
32
+ schema.methods[key] = { type: 'method', schema, ...method, name: key };
33
+ }
34
+ return schema;
35
+ }
36
+
37
+ /** A method exposed by a service. */
38
+ export interface ServiceMethodSchema extends SchemaBase {
39
+ type: 'method';
40
+ schema : ServiceSchema;
41
+ args: DtoMessage,
42
+ results : DtoMessage
21
43
  }
@@ -0,0 +1,186 @@
1
+ import { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
2
+ import type { FieldRuleEnd, FieldRuleSchema } from './field-rule.js';
3
+ import type { ThirdApiSchema } from './project.js';
4
+
5
+ // Third-party integration services.
6
+ // A ThirdMethodSchema is the Field-collection counterpart of a DtoMessage —
7
+ // like TableSchema.columns but for wire-format fields instead of DB columns.
8
+ // DTOs project from it via from(thirdMethod), and its fields may either share
9
+ // a local entity column instance (same fact, same type) or declare their own
10
+ // wire type plus a refs link (same fact, different type/format).
11
+
12
+ /** A third-party integration service (e.g. tenpay wechat pay).
13
+ * Distinct from ServiceSchema (backend service bound to an app) and
14
+ * ThirdApiSchema (project topology: the dir the integration lives in).
15
+ * Describes the adapter class contract: constructor config + methods. */
16
+ export interface ThirdServiceSchema extends SchemaBase {
17
+ type: 'thirdService';
18
+ /** The third-party system this service integrates (topology reference). */
19
+ schema: ThirdApiSchema;
20
+ /** Methods keyed by name — the map key is written back as the method name. */
21
+ methods: Record<string, ThirdServiceMethodSchema>;
22
+ }
23
+
24
+ /** A method exposed by a third-party service. */
25
+ export interface ThirdServiceMethodSchema extends SchemaBase {
26
+ type: 'method';
27
+ schema: ThirdServiceSchema;
28
+ /** Input message — the Field-collection counterpart of a DtoMessage. */
29
+ args: ThirdMethodSchema;
30
+ /** Output message. */
31
+ results: ThirdMethodSchema;
32
+ }
33
+
34
+ /** Field binding to a rule: which end the local wire field stands on.
35
+ * The ref always stands on the other end — from/to carry no extra information. */
36
+ export interface ConvertFieldSchema {
37
+ /** The rule binding field and ref (rule = name + two ends). */
38
+ rule: FieldRuleSchema;
39
+ /** The end instance the local wire field stands on. */
40
+ end: FieldRuleEnd;
41
+ }
42
+
43
+ /** Same-fact variant link: a wire field carrying the same fact as a local
44
+ * entity column under a different type/format (e.g. total_fee in fen vs amount in yuan). */
45
+ export interface ThirdFieldRef {
46
+ /** Wire field defined in this message (local definition). */
47
+ field: Field;
48
+ /** Field in another schema (table column or another message). */
49
+ ref: Field;
50
+ /** Optional rule binding — omitted when the fact is merely linked, not converted. */
51
+ convert?: ConvertFieldSchema;
52
+ }
53
+
54
+ /** A directional message of a third-party method: a Field collection mirroring
55
+ * TableSchema.columns, but fields hold wire-format names/types. A field may be
56
+ * a shared instance of a local entity column (same fact, same type) — its
57
+ * name/schema keep pointing at the table and the DTO projection inherits
58
+ * type/semantics from the entity, exactly like from(table). */
59
+ export interface ThirdMethodSchema extends CollectionSchemaBase {
60
+ type: 'thirdMethod';
61
+ /** The method this message belongs to (direction implied by args/results slot). */
62
+ schema: ThirdServiceMethodSchema;
63
+ /** Wire-format fields. */
64
+ fields: Record<string, Field>;
65
+ /** Same-fact variant links: wire field -> local entity column. */
66
+ refs?: ThirdFieldRef[];
67
+ }
68
+
69
+ /** Message input for defineThirdMethod: type/schema are set by the builder. */
70
+ export type ThirdMethodDef = Omit<ThirdMethodSchema, 'type' | 'schema'>;
71
+
72
+ /** Build a third-party method message. Writes back name/schema on own fields
73
+ * (top-level and nested); shared entity columns keep their table identity and
74
+ * must be keyed by their column name. */
75
+ export function defineThirdMethod(def: ThirdMethodDef): ThirdMethodSchema {
76
+ const message: ThirdMethodSchema = {
77
+ type: 'thirdMethod',
78
+ name: def.name,
79
+ description: def.description,
80
+ // Filled by defineThirdService.
81
+ schema: undefined as unknown as ThirdServiceMethodSchema,
82
+ fields: def.fields,
83
+ refs: def.refs,
84
+ };
85
+ for (const key of Object.keys(message.fields)) {
86
+ const field = message.fields[key] as Field;
87
+ if (field.schema === undefined) {
88
+ field.name = key;
89
+ field.schema = message;
90
+ } else if (field.schema.type === 'table') {
91
+ // Shared entity column: from() names the projection after field.name, so
92
+ // a mismatched key would silently rename the wire field. Same-fact fields
93
+ // with different names go through refs instead.
94
+ if (field.name !== key) {
95
+ throw new Error(
96
+ `thirdMethod '${message.name}': shared column key '${key}' must match the column name '${field.name}' — ` +
97
+ `same-fact fields with different names go through refs instead`,
98
+ );
99
+ }
100
+ } else if (field.schema !== message) {
101
+ throw new Error(
102
+ `thirdMethod '${message.name}': field '${key}' already belongs to ${field.schema.type} '${field.schema.name}', cannot reuse`,
103
+ );
104
+ }
105
+ writeBackNested(message, field);
106
+ }
107
+ if (message.refs !== undefined) {
108
+ const ownFields = Object.values(message.fields);
109
+ for (const link of message.refs) {
110
+ if (!ownFields.includes(link.field)) {
111
+ throw new Error(`thirdMethod '${message.name}': ref field must be one of its fields`);
112
+ }
113
+ if (link.ref.schema === undefined || link.ref.schema === message) {
114
+ throw new Error(`thirdMethod '${message.name}': ref target '${link.ref.name}' must be defined in another schema`);
115
+ }
116
+ if (link.convert !== undefined) {
117
+ const ends = Object.values(link.convert.rule.ends);
118
+ if (!ends.includes(link.convert.end)) {
119
+ throw new Error(
120
+ `thirdMethod '${message.name}': convert end '${link.convert.end.name}' must be one of rule '${link.convert.rule.name}' ends`,
121
+ );
122
+ }
123
+ }
124
+ }
125
+ }
126
+ return message;
127
+ }
128
+
129
+ /** Write back name/schema on nested wire fields (array items, object properties);
130
+ * shared entity columns keep their table identity. */
131
+ function writeBackNested(message: ThirdMethodSchema, field: Field): void {
132
+ if (field.type === 'array') {
133
+ writeBackNested(message, field.items);
134
+ return;
135
+ }
136
+ if (field.type !== 'object') return;
137
+ for (const key of Object.keys(field.properties)) {
138
+ const child = field.properties[key] as Field;
139
+ if (child.schema === undefined) {
140
+ child.name = key;
141
+ child.schema = message;
142
+ }
143
+ writeBackNested(message, child);
144
+ }
145
+ }
146
+
147
+ /** Method input for defineThirdService: name is written back from the methods map key. */
148
+ export interface ThirdServiceMethodDef {
149
+ /** Input message. */
150
+ args: ThirdMethodDef;
151
+ /** Output message. */
152
+ results: ThirdMethodDef;
153
+ description?: string;
154
+ }
155
+
156
+ export function defineThirdService(options: {
157
+ schema: ThirdApiSchema;
158
+ name: string;
159
+ methods: Record<string, ThirdServiceMethodDef>;
160
+ description?: string;
161
+ }): ThirdServiceSchema {
162
+ const schema: ThirdServiceSchema = {
163
+ type: 'thirdService',
164
+ name: options.name,
165
+ description: options.description,
166
+ schema: options.schema,
167
+ methods: {},
168
+ };
169
+ for (const key of Object.keys(options.methods)) {
170
+ const method = options.methods[key] as ThirdServiceMethodDef;
171
+ const methodSchema: ThirdServiceMethodSchema = {
172
+ type: 'method',
173
+ name: key,
174
+ description: method.description,
175
+ schema,
176
+ args: undefined as unknown as ThirdMethodSchema,
177
+ results: undefined as unknown as ThirdMethodSchema,
178
+ };
179
+ methodSchema.args = defineThirdMethod(method.args);
180
+ methodSchema.results = defineThirdMethod(method.results);
181
+ methodSchema.args.schema = methodSchema;
182
+ methodSchema.results.schema = methodSchema;
183
+ schema.methods[key] = methodSchema;
184
+ }
185
+ return schema;
186
+ }
@@ -1,5 +1,6 @@
1
1
  import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef } from './dto.js';
2
- import { Field } from './dsl.js';
2
+ import { EnumField, Field } from './dsl.js';
3
+ import type { ThirdMethodSchema } from './third-service.js';
3
4
 
4
5
  // TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
5
6
  // Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
@@ -27,6 +28,7 @@ function renderBasic(
27
28
  pattern: string | undefined,
28
29
  defaultValue: unknown,
29
30
  resolver: EnumResolver | undefined,
31
+ indent = 0,
30
32
  ): string {
31
33
  if (pattern !== undefined && field.type !== 'string') {
32
34
  throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
@@ -77,15 +79,35 @@ function renderBasic(
77
79
  }
78
80
  return `Type.Enum(${ref.name})`;
79
81
  }
82
+ case 'array':
83
+ return `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
84
+ case 'object':
85
+ return renderFieldObject(field.properties, indent + 1, resolver);
80
86
  default:
81
87
  // Field union is exhaustive; this branch is unreachable at runtime.
82
88
  throw new Error(`unsupported field type: ${String((field as Field).type)}`);
83
89
  }
84
90
  }
85
91
 
86
- function renderObject(fields: Record<string, DtoField>, indent: number, resolver: EnumResolver | undefined): string {
92
+ /** Render a plain Field value (wire-format nested fields), wrapping optional. */
93
+ function renderFieldValue(field: Field, indent: number, resolver: EnumResolver | undefined): string {
94
+ const base = renderBasic(field, undefined, undefined, resolver, indent);
95
+ return field.optional ? `Type.Optional(${base})` : base;
96
+ }
97
+
98
+ /** Render a plain Field object (wire-format nested object). */
99
+ function renderFieldObject(properties: Record<string, Field>, indent: number, resolver: EnumResolver | undefined): string {
100
+ const pad = ' '.repeat(indent);
101
+ const entries = Object.entries(properties).map(([name, f]) => `${pad}${name}: ${renderFieldValue(f, indent, resolver)}`);
102
+ return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
103
+ }
104
+
105
+ function renderObject(fields: Record<string, DtoField | Field>, indent: number, resolver: EnumResolver | undefined): string {
87
106
  const pad = ' '.repeat(indent);
88
- const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
107
+ const entries = Object.entries(fields).map(([name, f]) => {
108
+ const rendered = isDtoField(f) ? renderField(f, indent, resolver) : renderFieldValue(f, indent, resolver);
109
+ return `${pad}${name}: ${rendered}`;
110
+ });
89
111
  return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
90
112
  }
91
113
 
@@ -99,7 +121,8 @@ function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undef
99
121
  const items = f.field.items;
100
122
  // Referenced DTO element — render by name (same-file export), not expanded.
101
123
  if (isDtoMessage(items)) return `Type.Array(${items.name})`;
102
- return `Type.Array(${renderField(items, indent + 1, resolver)})`;
124
+ if (isDtoField(items)) return `Type.Array(${renderField(items, indent + 1, resolver)})`;
125
+ return `Type.Array(${renderFieldValue(items, indent, resolver)})`;
103
126
  }
104
127
  if (f.field.type === 'object') {
105
128
  return renderObject(f.field.properties, indent + 1, resolver);
@@ -107,7 +130,7 @@ function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undef
107
130
  // DtoField only wraps a database Field; array/object defs live in the subclasses.
108
131
  // Only DTO-level defaults (setDefault) are emitted as TypeBox default
109
132
  // annotations; DB field defaults are not carried into the API contract.
110
- return renderBasic(f.field as Field, f.pattern, f.default, resolver);
133
+ return renderBasic(f.field as Field, f.pattern, f.default, resolver, indent);
111
134
  }
112
135
 
113
136
  /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
@@ -116,6 +139,12 @@ function isDtoMessage(v: unknown): v is DtoMessage {
116
139
  return (v as Record<string, unknown>).type === 'dto';
117
140
  }
118
141
 
142
+ /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
143
+ function isDtoField(v: unknown): v is DtoField {
144
+ if (typeof v !== 'object' || v === null) return false;
145
+ return 'field' in v && !('type' in v);
146
+ }
147
+
119
148
  function collectEnumImports(
120
149
  f: DtoField,
121
150
  resolver: EnumResolver | undefined,
@@ -123,18 +152,45 @@ function collectEnumImports(
123
152
  ): void {
124
153
  if (f.field.type === 'array') {
125
154
  const items = f.field.items;
126
- if (!isDtoMessage(items)) collectEnumImports(items, resolver, out);
155
+ if (isDtoMessage(items)) return;
156
+ if (isDtoField(items)) {
157
+ collectEnumImports(items, resolver, out);
158
+ return;
159
+ }
160
+ collectFieldEnumImports(items, resolver, out);
127
161
  return;
128
162
  }
129
163
  if (f.field.type === 'object') {
130
- for (const child of Object.values(f.field.properties)) collectEnumImports(child, resolver, out);
164
+ for (const child of Object.values(f.field.properties)) {
165
+ if (isDtoField(child)) collectEnumImports(child, resolver, out);
166
+ else collectFieldEnumImports(child, resolver, out);
167
+ }
131
168
  return;
132
169
  }
133
- if (f.field.type === 'enum') {
134
- const ref = resolver?.(f.field.enum.jsName);
135
- if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
136
- out.set(`${ref.from}#${ref.name}`, ref);
170
+ if (f.field.type === 'enum') collectEnumRef(f.field, resolver, out);
171
+ }
172
+
173
+ /** Enum import collection over a plain Field (wire-format nested fields). */
174
+ function collectFieldEnumImports(
175
+ field: Field,
176
+ resolver: EnumResolver | undefined,
177
+ out: Map<string, ImportBase>,
178
+ ): void {
179
+ if (field.type === 'array') {
180
+ collectFieldEnumImports(field.items, resolver, out);
181
+ return;
137
182
  }
183
+ if (field.type === 'object') {
184
+ for (const child of Object.values(field.properties)) collectFieldEnumImports(child, resolver, out);
185
+ return;
186
+ }
187
+ if (field.type === 'enum') collectEnumRef(field, resolver, out);
188
+ }
189
+
190
+ function collectEnumRef(field: EnumField, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void {
191
+ const ref = resolver?.(field.enum.jsName);
192
+ if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
193
+ out.set(`${ref.from}#${ref.name}`, ref);
138
194
  }
139
195
 
140
196
  /** Collect all imports needed to render a DTO: include() bases + enum references. */
@@ -191,4 +247,19 @@ function renderBase(base: ImportRef): string {
191
247
  if (base.args === undefined || base.args.length === 0) return base.name;
192
248
  const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
193
249
  return `${base.name}(${args.join(', ')})`;
250
+ }
251
+
252
+ /** Render one third-party method message export (const only — pair with
253
+ * renderDtoTypeExport for the Static type). */
254
+ export function renderThirdMethodExport(schema: ThirdMethodSchema, resolver: EnumResolver | undefined): string {
255
+ return `export const ${schema.name} = ${renderFieldObject(schema.fields, 1, resolver)};`;
256
+ }
257
+
258
+ /** Collect all imports needed to render a third-party method message: enum references. */
259
+ export function collectThirdMethodImports(
260
+ schema: ThirdMethodSchema,
261
+ resolver: EnumResolver | undefined,
262
+ out: Map<string, ImportBase>,
263
+ ): void {
264
+ for (const f of Object.values(schema.fields)) collectFieldEnumImports(f, resolver, out);
194
265
  }
package/src/utils.ts CHANGED
@@ -1,27 +1,64 @@
1
- // ── naming conversions ──
2
-
3
- /** snake_case → camelCase: mer_id → merId; names without underscores are unchanged */
4
- export function toCamelCase(name: string): string {
5
- return name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
6
- }
7
-
8
- /** snake_case PascalCase: mer_id MerId */
9
- export function toPascalCase(snake: string): string {
10
- return snake.replace(/(^|_)([a-z])/g, (_m, _p, c: string) => c.toUpperCase());
11
- }
12
-
13
- // ── schema ──
14
-
15
- import { SchemaBase } from './dsl.js';
16
- import { FrontAppSchema } from './project.js';
17
-
18
- /** A utility module bound to exactly one frontend app. */
19
- export interface UtilsSchema extends SchemaBase {
20
- type: 'utils';
21
- /** The frontend app this utility module belongs to (shared instance from project.config). */
22
- app: FrontAppSchema;
23
- }
24
-
25
- export function defineUtils(name: string, app: FrontAppSchema, description?: string): UtilsSchema {
26
- return { name, type: 'utils', app, description };
1
+ import type { Field, SchemaBase } from './dsl.js';
2
+ import type { FrontAppSchema } from './project.js';
3
+
4
+ // Base utility modules business-agnostic helpers with full method
5
+ // signatures (e.g. DateTimeUtils.format). Called at the service layer;
6
+ // their args/results are own wire types, independent of business schemas.
7
+
8
+ /** A utility method with a full signature. */
9
+ export interface UtilsMethodSchema extends SchemaBase {
10
+ type: 'utilsMethod';
11
+ /** The utility module this method belongs to. */
12
+ schema: UtilsSchema;
13
+ /** Input fields. */
14
+ args: Record<string, Field>;
15
+ /** Output field. */
16
+ result: Field;
17
+ }
18
+
19
+ /** Method input for defineUtils: type/schema/name are set by the builder. */
20
+ export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>;
21
+
22
+ /** A base utility module (e.g. DateTimeUtils). */
23
+ export interface UtilsSchema extends SchemaBase {
24
+ type: 'utils';
25
+ /** Optional binding empty means a shared public module. */
26
+ app?: FrontAppSchema;
27
+ /** Methods keyed by name — the map key is written back as the method name. */
28
+ methods: Record<string, UtilsMethodSchema>;
29
+ }
30
+
31
+ export function defineUtils(options: {
32
+ name: string;
33
+ app?: FrontAppSchema;
34
+ methods: Record<string, UtilsMethodDef>;
35
+ description?: string;
36
+ }): UtilsSchema {
37
+ const schema: UtilsSchema = {
38
+ type: 'utils',
39
+ name: options.name,
40
+ description: options.description,
41
+ app: options.app,
42
+ methods: {},
43
+ };
44
+ for (const key of Object.keys(options.methods)) {
45
+ const method = options.methods[key] as UtilsMethodDef;
46
+ const methodSchema: UtilsMethodSchema = {
47
+ type: 'utilsMethod',
48
+ name: key,
49
+ description: method.description,
50
+ schema,
51
+ args: method.args,
52
+ result: method.result,
53
+ };
54
+ for (const argKey of Object.keys(methodSchema.args)) {
55
+ const field = methodSchema.args[argKey] as Field;
56
+ field.name = argKey;
57
+ field.schema = methodSchema;
58
+ }
59
+ methodSchema.result.name = key;
60
+ methodSchema.result.schema = methodSchema;
61
+ schema.methods[key] = methodSchema;
62
+ }
63
+ return schema;
27
64
  }
@@ -1,9 +0,0 @@
1
- export interface InheritanceIssue {
2
- file: string;
3
- container: string;
4
- field: string;
5
- /** e.g. ["t_order.order_no"] or ["t_order.id", "t_merchant.id"] when several inferred tables share the name */
6
- candidates: string[];
7
- }
8
- /** Check one loaded DSL module (its exports) for inheritance gaps. */
9
- export declare function checkInheritance(mod: Record<string, unknown>, file: string): InheritanceIssue[];