@pylonts/dsl 1.1.5 → 1.1.11

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 (100) hide show
  1. package/README.md +4 -0
  2. package/dist/action.d.ts +32 -0
  3. package/dist/action.js +14 -0
  4. package/dist/aggregate.d.ts +38 -0
  5. package/dist/aggregate.js +46 -0
  6. package/dist/business-flow.d.ts +9 -0
  7. package/dist/business-flow.js +72 -0
  8. package/dist/controller.d.ts +35 -0
  9. package/dist/controller.js +17 -0
  10. package/dist/convert.d.ts +37 -7
  11. package/dist/convert.js +23 -2
  12. package/dist/curd.d.ts +7 -12
  13. package/dist/curd.js +10 -1
  14. package/dist/dao.d.ts +140 -3
  15. package/dist/dao.js +307 -2
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +13 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +32 -2
  21. package/dist/dsl.js +13 -0
  22. package/dist/dto.d.ts +8 -2
  23. package/dist/dto.js +21 -9
  24. package/dist/endpoint.d.ts +15 -0
  25. package/dist/endpoint.js +3 -0
  26. package/dist/entity.d.ts +29 -0
  27. package/dist/entity.js +13 -0
  28. package/dist/exception.d.ts +20 -0
  29. package/dist/exception.js +33 -0
  30. package/dist/expr.d.ts +45 -0
  31. package/dist/expr.js +32 -0
  32. package/dist/field-rule.d.ts +20 -0
  33. package/dist/field-rule.js +19 -0
  34. package/dist/filter.d.ts +45 -0
  35. package/dist/filter.js +21 -0
  36. package/dist/flow-script.d.ts +108 -0
  37. package/dist/flow-script.js +505 -0
  38. package/dist/flow.d.ts +309 -10
  39. package/dist/flow.js +819 -22
  40. package/dist/index.d.ts +12 -1
  41. package/dist/index.js +14 -1
  42. package/dist/mermaid-driver.js +278 -9
  43. package/dist/method.d.ts +11 -0
  44. package/dist/method.js +3 -0
  45. package/dist/mysql-driver.js +7 -0
  46. package/dist/project.d.ts +5 -4
  47. package/dist/project.js +14 -2
  48. package/dist/provider.d.ts +6 -11
  49. package/dist/provider.js +2 -2
  50. package/dist/repository.d.ts +26 -0
  51. package/dist/repository.js +8 -0
  52. package/dist/service.d.ts +30 -8
  53. package/dist/service.js +62 -2
  54. package/dist/third-service.d.ts +80 -0
  55. package/dist/third-service.js +97 -0
  56. package/dist/typebox-driver.d.ts +6 -0
  57. package/dist/typebox-driver.js +77 -12
  58. package/dist/utils.d.ts +32 -10
  59. package/dist/utils.js +32 -11
  60. package/docs/aggregate.md +110 -0
  61. package/docs/dao-generation.md +478 -0
  62. package/docs/ddd-principles.md +75 -0
  63. package/docs/domain-event.md +137 -0
  64. package/docs/dto.md +73 -66
  65. package/docs/keyword-matcher.md +182 -0
  66. package/docs/third-service.md +122 -0
  67. package/docs/token.md +327 -0
  68. package/docs/trans-reentrant.md +85 -0
  69. package/package.json +27 -5
  70. package/src/action.ts +51 -10
  71. package/src/aggregate.ts +104 -0
  72. package/src/business-flow.ts +80 -0
  73. package/src/controller.ts +54 -0
  74. package/src/convert.ts +78 -15
  75. package/src/curd.ts +104 -93
  76. package/src/dao.ts +486 -13
  77. package/src/db.ts +199 -181
  78. package/src/domain-event.ts +74 -0
  79. package/src/dsl.ts +48 -2
  80. package/src/dto.ts +266 -247
  81. package/src/entity.ts +43 -0
  82. package/src/exception.ts +53 -0
  83. package/src/expr.ts +65 -0
  84. package/src/field-rule.ts +47 -0
  85. package/src/filter.ts +70 -0
  86. package/src/flow-script.ts +696 -0
  87. package/src/flow.ts +1226 -103
  88. package/src/index.ts +47 -33
  89. package/src/mermaid-driver.ts +339 -84
  90. package/src/method.ts +20 -0
  91. package/src/mysql-driver.ts +7 -0
  92. package/src/project.ts +114 -97
  93. package/src/repository.ts +35 -0
  94. package/src/service.ts +107 -20
  95. package/src/third-service.ts +192 -0
  96. package/src/typebox-driver.ts +86 -11
  97. package/src/utils.ts +74 -26
  98. package/dist/check-inheritance.d.ts +0 -9
  99. package/dist/check-inheritance.js +0 -58
  100. package/src/provider.ts +0 -73
package/dist/service.js CHANGED
@@ -1,3 +1,63 @@
1
- export function defineService(name, app, description) {
2
- return { name, type: 'service', app, description };
1
+ import { exceptionEndNames } from './flow.js';
2
+ export function defineService(options) {
3
+ if (!options.api.apps.includes(options.app)) {
4
+ throw new Error(`service ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
5
+ }
6
+ const schema = {
7
+ type: 'service',
8
+ name: options.name,
9
+ description: options.description,
10
+ api: options.api,
11
+ app: options.app,
12
+ methods: {},
13
+ };
14
+ for (const key of Object.keys(options.methods)) {
15
+ const method = options.methods[key];
16
+ schema.methods[key] = { type: 'method', schema, ...method, name: key };
17
+ }
18
+ validateFlowBindings(schema);
19
+ return schema;
20
+ }
21
+ // A method bound to a flow: the flow's escape set (its exception ends) must
22
+ // equal the method's declared throws, the flow's own args/results (when
23
+ // present) must be the same objects as the method's, and one flow may serve
24
+ // only one method.
25
+ //
26
+ // Notes: (1) the unique-binding check is per defineService call — a flow
27
+ // shared across two service schemas is not detected; (2) escape-set equality
28
+ // means exceptions swallowed by an internal catch (or routed through a
29
+ // catch-all end) disappear from the contract, so the method must not declare
30
+ // them.
31
+ function validateFlowBindings(schema) {
32
+ const seenFlows = new Set();
33
+ for (const key of Object.keys(schema.methods)) {
34
+ const m = schema.methods[key];
35
+ if (!m.flow)
36
+ continue;
37
+ if (seenFlows.has(m.flow)) {
38
+ throw new Error(`service ${schema.name}: flow "${m.flow.name}" is bound to more than one method`);
39
+ }
40
+ seenFlows.add(m.flow);
41
+ if (m.flow.args !== undefined && m.flow.args !== m.args) {
42
+ throw new Error(`service ${schema.name}: method "${key}" args and its flow "${m.flow.name}" args must be the same object`);
43
+ }
44
+ if (m.flow.results !== undefined && m.flow.results !== m.results) {
45
+ throw new Error(`service ${schema.name}: method "${key}" results and its flow "${m.flow.name}" results must be the same object`);
46
+ }
47
+ const escaped = exceptionEndNames(m.flow);
48
+ const declared = new Set((m.throws ?? []).map((t) => t.name));
49
+ for (const e of escaped) {
50
+ if (!declared.has(e)) {
51
+ throw new Error(`service ${schema.name}: method "${key}" flow escapes ${e} but the method does not declare it`);
52
+ }
53
+ }
54
+ for (const d of declared) {
55
+ if (!escaped.has(d)) {
56
+ throw new Error(`service ${schema.name}: method "${key}" declares throw ${d} but its flow never escapes it`);
57
+ }
58
+ }
59
+ if (m.flow.name !== key) {
60
+ console.warn(`service ${schema.name}: method "${key}" flow name "${m.flow.name}" differs from the method key`);
61
+ }
62
+ }
3
63
  }
@@ -0,0 +1,80 @@
1
+ import { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
2
+ import type { ExceptionSchema } from './exception.js';
3
+ import type { FieldRuleEnd, FieldRuleSchema } from './field-rule.js';
4
+ import type { ThirdApiSchema } from './project.js';
5
+ /** A third-party integration service (e.g. tenpay wechat pay).
6
+ * Distinct from ServiceSchema (backend service bound to an app) and
7
+ * ThirdApiSchema (project topology: the dir the integration lives in).
8
+ * Describes the adapter class contract: constructor config + methods. */
9
+ export interface ThirdServiceSchema extends SchemaBase {
10
+ type: 'thirdService';
11
+ /** The third-party system this service integrates (topology reference). */
12
+ schema: ThirdApiSchema;
13
+ /** Methods keyed by name — the map key is written back as the method name. */
14
+ methods: Record<string, ThirdServiceMethodSchema>;
15
+ }
16
+ /** A method exposed by a third-party service. */
17
+ export interface ThirdServiceMethodSchema extends SchemaBase {
18
+ type: 'method';
19
+ schema: ThirdServiceSchema;
20
+ /** Input message — the Field-collection counterpart of a DtoMessage. */
21
+ args: ThirdMethodSchema;
22
+ /** Output message. */
23
+ results: ThirdMethodSchema;
24
+ /** Exceptions this method may throw (e.g. IOException, CodeException). */
25
+ throws?: ExceptionSchema[];
26
+ }
27
+ /** Field binding to a rule: which end the local wire field stands on.
28
+ * The ref always stands on the other end — from/to carry no extra information. */
29
+ export interface ConvertFieldSchema {
30
+ /** The rule binding field and ref (rule = name + two ends). */
31
+ rule: FieldRuleSchema;
32
+ /** The end instance the local wire field stands on. */
33
+ end: FieldRuleEnd;
34
+ }
35
+ /** Same-fact variant link: a wire field carrying the same fact as a local
36
+ * entity column under a different type/format (e.g. total_fee in fen vs amount in yuan). */
37
+ export interface ThirdFieldRef {
38
+ /** Wire field defined in this message (local definition). */
39
+ field: Field;
40
+ /** Field in another schema (table column or another message). */
41
+ ref: Field;
42
+ /** Optional rule binding — omitted when the fact is merely linked, not converted. */
43
+ convert?: ConvertFieldSchema;
44
+ }
45
+ /** A directional message of a third-party method: a Field collection mirroring
46
+ * TableSchema.columns, but fields hold wire-format names/types. A field may be
47
+ * a shared instance of a local entity column (same fact, same type) — its
48
+ * name/schema keep pointing at the table and the DTO projection inherits
49
+ * type/semantics from the entity, exactly like from(table). */
50
+ export interface ThirdMethodSchema extends CollectionSchemaBase {
51
+ type: 'thirdMethod';
52
+ /** The method this message belongs to (direction implied by args/results slot). */
53
+ schema: ThirdServiceMethodSchema;
54
+ /** Wire-format fields. */
55
+ fields: Record<string, Field>;
56
+ /** Same-fact variant links: wire field -> local entity column. */
57
+ refs?: ThirdFieldRef[];
58
+ }
59
+ /** Message input for defineThirdMethod: type/schema are set by the builder. */
60
+ export type ThirdMethodDef = Omit<ThirdMethodSchema, 'type' | 'schema'>;
61
+ /** Build a third-party method message. Writes back name/schema on own fields
62
+ * (top-level and nested); shared entity columns keep their table identity and
63
+ * must be keyed by their column name. */
64
+ export declare function defineThirdMethod(def: ThirdMethodDef): ThirdMethodSchema;
65
+ /** Method input for defineThirdService: name is written back from the methods map key. */
66
+ export interface ThirdServiceMethodDef {
67
+ /** Input message. */
68
+ args: ThirdMethodDef;
69
+ /** Output message. */
70
+ results: ThirdMethodDef;
71
+ /** Exceptions this method may throw (e.g. IOException, CodeException). */
72
+ throws?: ExceptionSchema[];
73
+ description?: string;
74
+ }
75
+ export declare function defineThirdService(options: {
76
+ schema: ThirdApiSchema;
77
+ name: string;
78
+ methods: Record<string, ThirdServiceMethodDef>;
79
+ description?: string;
80
+ }): ThirdServiceSchema;
@@ -0,0 +1,97 @@
1
+ /** Build a third-party method message. Writes back name/schema on own fields
2
+ * (top-level and nested); shared entity columns keep their table identity and
3
+ * must be keyed by their column name. */
4
+ export function defineThirdMethod(def) {
5
+ const message = {
6
+ type: 'thirdMethod',
7
+ name: def.name,
8
+ description: def.description,
9
+ // Filled by defineThirdService.
10
+ schema: undefined,
11
+ fields: def.fields,
12
+ refs: def.refs,
13
+ };
14
+ for (const key of Object.keys(message.fields)) {
15
+ const field = message.fields[key];
16
+ if (field.schema === undefined) {
17
+ field.name = key;
18
+ field.schema = message;
19
+ }
20
+ else if (field.schema.type === 'table') {
21
+ // Shared entity column: from() names the projection after field.name, so
22
+ // a mismatched key would silently rename the wire field. Same-fact fields
23
+ // with different names go through refs instead.
24
+ if (field.name !== key) {
25
+ throw new Error(`thirdMethod '${message.name}': shared column key '${key}' must match the column name '${field.name}' — ` +
26
+ `same-fact fields with different names go through refs instead`);
27
+ }
28
+ }
29
+ else if (field.schema !== message) {
30
+ throw new Error(`thirdMethod '${message.name}': field '${key}' already belongs to ${field.schema.type} '${field.schema.name}', cannot reuse`);
31
+ }
32
+ writeBackNested(message, field);
33
+ }
34
+ if (message.refs !== undefined) {
35
+ const ownFields = Object.values(message.fields);
36
+ for (const link of message.refs) {
37
+ if (!ownFields.includes(link.field)) {
38
+ throw new Error(`thirdMethod '${message.name}': ref field must be one of its fields`);
39
+ }
40
+ if (link.ref.schema === undefined || link.ref.schema === message) {
41
+ throw new Error(`thirdMethod '${message.name}': ref target '${link.ref.name}' must be defined in another schema`);
42
+ }
43
+ if (link.convert !== undefined) {
44
+ const ends = Object.values(link.convert.rule.ends);
45
+ if (!ends.includes(link.convert.end)) {
46
+ throw new Error(`thirdMethod '${message.name}': convert end '${link.convert.end.name}' must be one of rule '${link.convert.rule.name}' ends`);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return message;
52
+ }
53
+ /** Write back name/schema on nested wire fields (array items, object properties);
54
+ * shared entity columns keep their table identity. */
55
+ function writeBackNested(message, field) {
56
+ if (field.type === 'array') {
57
+ writeBackNested(message, field.items);
58
+ return;
59
+ }
60
+ if (field.type !== 'object')
61
+ return;
62
+ for (const key of Object.keys(field.properties)) {
63
+ const child = field.properties[key];
64
+ if (child.schema === undefined) {
65
+ child.name = key;
66
+ child.schema = message;
67
+ }
68
+ writeBackNested(message, child);
69
+ }
70
+ }
71
+ export function defineThirdService(options) {
72
+ const schema = {
73
+ type: 'thirdService',
74
+ name: options.name,
75
+ description: options.description,
76
+ schema: options.schema,
77
+ methods: {},
78
+ };
79
+ for (const key of Object.keys(options.methods)) {
80
+ const method = options.methods[key];
81
+ const methodSchema = {
82
+ type: 'method',
83
+ name: key,
84
+ description: method.description,
85
+ schema,
86
+ args: undefined,
87
+ results: undefined,
88
+ throws: method.throws,
89
+ };
90
+ methodSchema.args = defineThirdMethod(method.args);
91
+ methodSchema.results = defineThirdMethod(method.results);
92
+ methodSchema.args.schema = methodSchema;
93
+ methodSchema.results.schema = methodSchema;
94
+ schema.methods[key] = methodSchema;
95
+ }
96
+ return schema;
97
+ }
@@ -1,4 +1,5 @@
1
1
  import { DtoMessage, ImportBase } from './dto.js';
2
+ import type { ThirdMethodSchema } from './third-service.js';
2
3
  export type EnumResolver = (enumName: string) => ImportBase | undefined;
3
4
  /** Collect all imports needed to render a DTO: include() bases + enum references. */
4
5
  export declare function collectDtoImports(schema: DtoMessage, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void;
@@ -10,3 +11,8 @@ export declare function renderDtoMessage(schema: DtoMessage, options?: {
10
11
  resolver?: EnumResolver;
11
12
  source?: string;
12
13
  }): string;
14
+ /** Render one third-party method message export (const only — pair with
15
+ * renderDtoTypeExport for the Static type). */
16
+ export declare function renderThirdMethodExport(schema: ThirdMethodSchema, resolver: EnumResolver | undefined): string;
17
+ /** Collect all imports needed to render a third-party method message: enum references. */
18
+ export declare function collectThirdMethodImports(schema: ThirdMethodSchema, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void;
@@ -6,7 +6,7 @@ function renderDefault(v) {
6
6
  return renderString(v);
7
7
  return JSON.stringify(v);
8
8
  }
9
- function renderBasic(field, pattern, defaultValue, resolver) {
9
+ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
10
10
  if (pattern !== undefined && field.type !== 'string') {
11
11
  throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
12
12
  }
@@ -62,14 +62,36 @@ function renderBasic(field, pattern, defaultValue, resolver) {
62
62
  }
63
63
  return `Type.Enum(${ref.name})`;
64
64
  }
65
+ case 'aggregate':
66
+ // Aggregate query outputs: count/sum(int) are numbers, everything else
67
+ // arrives as a precision string.
68
+ return field.jsType === 'number' ? 'Type.Number()' : 'Type.String()';
69
+ case 'array':
70
+ return `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
71
+ case 'object':
72
+ return renderFieldObject(field.properties, indent + 1, resolver);
65
73
  default:
66
74
  // Field union is exhaustive; this branch is unreachable at runtime.
67
75
  throw new Error(`unsupported field type: ${String(field.type)}`);
68
76
  }
69
77
  }
78
+ /** Render a plain Field value (wire-format nested fields), wrapping optional. */
79
+ function renderFieldValue(field, indent, resolver) {
80
+ const base = renderBasic(field, undefined, undefined, resolver, indent);
81
+ return field.optional ? `Type.Optional(${base})` : base;
82
+ }
83
+ /** Render a plain Field object (wire-format nested object). */
84
+ function renderFieldObject(properties, indent, resolver) {
85
+ const pad = ' '.repeat(indent);
86
+ const entries = Object.entries(properties).map(([name, f]) => `${pad}${name}: ${renderFieldValue(f, indent, resolver)}`);
87
+ return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
88
+ }
70
89
  function renderObject(fields, indent, resolver) {
71
90
  const pad = ' '.repeat(indent);
72
- const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
91
+ const entries = Object.entries(fields).map(([name, f]) => {
92
+ const rendered = isDtoField(f) ? renderField(f, indent, resolver) : renderFieldValue(f, indent, resolver);
93
+ return `${pad}${name}: ${rendered}`;
94
+ });
73
95
  return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
74
96
  }
75
97
  function renderField(f, indent, resolver) {
@@ -82,7 +104,9 @@ function renderValue(f, indent, resolver) {
82
104
  // Referenced DTO element — render by name (same-file export), not expanded.
83
105
  if (isDtoMessage(items))
84
106
  return `Type.Array(${items.name})`;
85
- return `Type.Array(${renderField(items, indent + 1, resolver)})`;
107
+ if (isDtoField(items))
108
+ return `Type.Array(${renderField(items, indent + 1, resolver)})`;
109
+ return `Type.Array(${renderFieldValue(items, indent, resolver)})`;
86
110
  }
87
111
  if (f.field.type === 'object') {
88
112
  return renderObject(f.field.properties, indent + 1, resolver);
@@ -90,7 +114,7 @@ function renderValue(f, indent, resolver) {
90
114
  // DtoField only wraps a database Field; array/object defs live in the subclasses.
91
115
  // Only DTO-level defaults (setDefault) are emitted as TypeBox default
92
116
  // annotations; DB field defaults are not carried into the API contract.
93
- return renderBasic(f.field, f.pattern, f.default, resolver);
117
+ return renderBasic(f.field, f.pattern, f.default, resolver, indent);
94
118
  }
95
119
  /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
96
120
  function isDtoMessage(v) {
@@ -98,24 +122,55 @@ function isDtoMessage(v) {
98
122
  return false;
99
123
  return v.type === 'dto';
100
124
  }
125
+ /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
126
+ function isDtoField(v) {
127
+ if (typeof v !== 'object' || v === null)
128
+ return false;
129
+ return 'field' in v && !('type' in v);
130
+ }
101
131
  function collectEnumImports(f, resolver, out) {
102
132
  if (f.field.type === 'array') {
103
133
  const items = f.field.items;
104
- if (!isDtoMessage(items))
134
+ if (isDtoMessage(items))
135
+ return;
136
+ if (isDtoField(items)) {
105
137
  collectEnumImports(items, resolver, out);
138
+ return;
139
+ }
140
+ collectFieldEnumImports(items, resolver, out);
106
141
  return;
107
142
  }
108
143
  if (f.field.type === 'object') {
109
- for (const child of Object.values(f.field.properties))
110
- collectEnumImports(child, resolver, out);
144
+ for (const child of Object.values(f.field.properties)) {
145
+ if (isDtoField(child))
146
+ collectEnumImports(child, resolver, out);
147
+ else
148
+ collectFieldEnumImports(child, resolver, out);
149
+ }
111
150
  return;
112
151
  }
113
- if (f.field.type === 'enum') {
114
- const ref = resolver?.(f.field.enum.jsName);
115
- if (!ref)
116
- throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
117
- out.set(`${ref.from}#${ref.name}`, ref);
152
+ if (f.field.type === 'enum')
153
+ collectEnumRef(f.field, resolver, out);
154
+ }
155
+ /** Enum import collection over a plain Field (wire-format nested fields). */
156
+ function collectFieldEnumImports(field, resolver, out) {
157
+ if (field.type === 'array') {
158
+ collectFieldEnumImports(field.items, resolver, out);
159
+ return;
160
+ }
161
+ if (field.type === 'object') {
162
+ for (const child of Object.values(field.properties))
163
+ collectFieldEnumImports(child, resolver, out);
164
+ return;
118
165
  }
166
+ if (field.type === 'enum')
167
+ collectEnumRef(field, resolver, out);
168
+ }
169
+ function collectEnumRef(field, resolver, out) {
170
+ const ref = resolver?.(field.enum.jsName);
171
+ if (!ref)
172
+ throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
173
+ out.set(`${ref.from}#${ref.name}`, ref);
119
174
  }
120
175
  /** Collect all imports needed to render a DTO: include() bases + enum references. */
121
176
  export function collectDtoImports(schema, resolver, out) {
@@ -161,3 +216,13 @@ function renderBase(base) {
161
216
  const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
162
217
  return `${base.name}(${args.join(', ')})`;
163
218
  }
219
+ /** Render one third-party method message export (const only — pair with
220
+ * renderDtoTypeExport for the Static type). */
221
+ export function renderThirdMethodExport(schema, resolver) {
222
+ return `export const ${schema.name} = ${renderFieldObject(schema.fields, 1, resolver)};`;
223
+ }
224
+ /** Collect all imports needed to render a third-party method message: enum references. */
225
+ export function collectThirdMethodImports(schema, resolver, out) {
226
+ for (const f of Object.values(schema.fields))
227
+ collectFieldEnumImports(f, resolver, out);
228
+ }
package/dist/utils.d.ts CHANGED
@@ -1,13 +1,35 @@
1
- /** snake_case camelCase: mer_id merId; names without underscores are unchanged */
2
- export declare function toCamelCase(name: string): string;
3
- /** snake_case PascalCase: mer_id MerId */
4
- export declare function toPascalCase(snake: string): string;
5
- import { SchemaBase } from './dsl.js';
6
- import { FrontAppSchema } from './project.js';
7
- /** A utility module bound to exactly one frontend app. */
1
+ import type { Field, SchemaBase } from './dsl.js';
2
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
+ /** A utility method with a full signature. */
4
+ export interface UtilsMethodSchema extends SchemaBase {
5
+ type: 'utilsMethod';
6
+ /** The utility module this method belongs to. */
7
+ schema: UtilsSchema;
8
+ /** Input fields. */
9
+ args: Record<string, Field>;
10
+ /** Output field. */
11
+ result: Field;
12
+ }
13
+ /** Method input for defineUtils: type/schema/name are set by the builder. */
14
+ export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>;
15
+ /** A base utility module (e.g. DateTimeUtils). */
8
16
  export interface UtilsSchema extends SchemaBase {
9
17
  type: 'utils';
10
- /** The frontend app this utility module belongs to (shared instance from project.config). */
11
- app: FrontAppSchema;
18
+ /** Backend binding the api module this utils belongs to (shared instance
19
+ * from project.config.ts apis). With `app` it serves that frontend
20
+ * ({api}/{app}/utils/); alone it is the api's public module
21
+ * ({api}/common/utils/). Unset = not backend-side. */
22
+ api?: ProjectApiSchema;
23
+ /** Optional binding — the frontend app this utils serves (shared instance
24
+ * from project.config.ts apps). Empty means a shared public module. */
25
+ app?: FrontAppSchema;
26
+ /** Methods keyed by name — the map key is written back as the method name. */
27
+ methods: Record<string, UtilsMethodSchema>;
12
28
  }
13
- export declare function defineUtils(name: string, app: FrontAppSchema, description?: string): UtilsSchema;
29
+ export declare function defineUtils(options: {
30
+ name: string;
31
+ api?: ProjectApiSchema;
32
+ app?: FrontAppSchema;
33
+ methods: Record<string, UtilsMethodDef>;
34
+ description?: string;
35
+ }): UtilsSchema;
package/dist/utils.js CHANGED
@@ -1,12 +1,33 @@
1
- // ── naming conversions ──
2
- /** snake_case camelCase: mer_id merId; names without underscores are unchanged */
3
- export function toCamelCase(name) {
4
- return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
5
- }
6
- /** snake_case → PascalCase: mer_id → MerId */
7
- export function toPascalCase(snake) {
8
- return snake.replace(/(^|_)([a-z])/g, (_m, _p, c) => c.toUpperCase());
9
- }
10
- export function defineUtils(name, app, description) {
11
- return { name, type: 'utils', app, description };
1
+ export function defineUtils(options) {
2
+ if (options.api !== undefined && options.app !== undefined && !options.api.apps.includes(options.app)) {
3
+ throw new Error(`utils ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
4
+ }
5
+ const schema = {
6
+ type: 'utils',
7
+ name: options.name,
8
+ description: options.description,
9
+ api: options.api,
10
+ app: options.app,
11
+ methods: {},
12
+ };
13
+ for (const key of Object.keys(options.methods)) {
14
+ const method = options.methods[key];
15
+ const methodSchema = {
16
+ type: 'utilsMethod',
17
+ name: key,
18
+ description: method.description,
19
+ schema,
20
+ args: method.args,
21
+ result: method.result,
22
+ };
23
+ for (const argKey of Object.keys(methodSchema.args)) {
24
+ const field = methodSchema.args[argKey];
25
+ field.name = argKey;
26
+ field.schema = methodSchema;
27
+ }
28
+ methodSchema.result.name = key;
29
+ methodSchema.result.schema = methodSchema;
30
+ schema.methods[key] = methodSchema;
31
+ }
32
+ return schema;
12
33
  }
@@ -0,0 +1,110 @@
1
+ # 聚合与仓储 DSL 扩展规划(Aggregate / Repository)
2
+
3
+ > 状态:**规划中(未实现)**
4
+ > 关联代码:`dsl/src/db.ts`(TableSchema)、`dsl/src/dao.ts`(DaoSchema)、`dsl/src/service.ts`(ServiceSchema)
5
+ > 背景对话:ts-libs 会话「dd DDD 扩展讨论」(商城下单用例)
6
+
7
+ ## 1. 背景:pylon 现状与 DDD 缺口
8
+
9
+ **现状**:
10
+
11
+ - `TableSchema` 是全局扁平数据字典("表无物理归属"),orders / order_items 是两张互相独立声明、无聚合关系的表;
12
+ - `DaoSchema` 是单表单 SQL(knex 透明代理),粒度 = 表,无跨表能力;
13
+ - 多表读写由 service flow 手工编排:`dao.insert(orders) → dao.insert(order_items)` + 手工 `@Trans()`,"总额 = Σ明细"这类一致性靠开发者自觉,无强制约束。
14
+
15
+ **DDD 缺口对照**(战术模式):
16
+
17
+ | DDD 概念 | dsl 现状 | 缺口 |
18
+ |---------|---------|------|
19
+ | 聚合 / 聚合根 | 无 | **全新概念** |
20
+ | 值对象 | mock 识别"金额/手机号"语义 | 无声明层 |
21
+ | 领域事件 | 只有 UI 组件事件(event.ts) | **全新概念** |
22
+ | 领域服务 | 全混在应用服务(service_schema) | 需拆分 |
23
+ | 仓储 | 无(DAO 粒度是表) | **全新概念** |
24
+ | 应用服务编排 | service_schema + flow ✅ | 已具备 |
25
+ | 防腐层 | ThirdServiceSchema 只隔离 | 缺模型映射 |
26
+
27
+ ## 2. 扩展一:AggregateSchema(核心)
28
+
29
+ **作用**:显式声明"哪些表属于同一个聚合、谁是聚合根、成员如何挂载、跨成员不变式、聚合间引用规则"。这是把"多表一致性从约定变约束"的落点。
30
+
31
+ ```ts
32
+ defineAggregate({
33
+ name: 'Order',
34
+ root: ordersTable, // 聚合根表
35
+ members: { // 成员表("包含几个 table schema" 的声明)
36
+ items: { table: orderItemsTable, via: 'order_no' }, // 1:N,外键挂根
37
+ address: { table: orderAddressTable, via: 'order_no', one: true }, // 1:1
38
+ },
39
+ invariants: [ // 跨成员表不变式,挂聚合上,可被生成代码消费
40
+ { name: 'total = sum(items.price * items.qty)',
41
+ check: 'totalAmount == sum(items.price * items.qty)' },
42
+ ],
43
+ references: { productId: 'ProductAggregate' }, // 聚合间只按 ID 引用
44
+ });
45
+ ```
46
+
47
+ | 声明项 | 含义 | 缺了会怎样 |
48
+ |--------|------|-----------|
49
+ | `root` | 谁是聚合根 | 分不清一致性入口 |
50
+ | `members` | 包含哪几个 table schema | "包含"只是列表,无结构 |
51
+ | `members[i].via` | 成员表靠哪个外键挂根 | 工具无法推导 join / 级联关系 |
52
+ | `invariants` | 跨成员一致性规则 | "总额=Σ明细"又回到 flow 里手工写 |
53
+ | `references` | 聚合间只按 ID 引用 | 无法 lint 跨聚合直接持表引用 |
54
+
55
+ **消费方**(声明一旦存在即可自动推导):
56
+
57
+ 1. **生成 Repository**:按"加载 / 保存 / 删除"三套固定骨架自动产出(见下),`orders + order_items` 自动同事务,不再手工 `@Trans()`;
58
+ 2. **lint 约束**:禁止聚合外代码直接 `insert/update/delete` 成员表(只准经 root 走);
59
+ 3. **不变式挂载**:save 前后强制校验。
60
+
61
+ **多表映射三种模式**(聚合↔表):A 单表=单聚合(1:1,几乎透明);B 一聚合=多表(1:N,最常见,Order 案例);C 多聚合共享表(N:1,DDD 不推荐)。
62
+
63
+ ## 3. 扩展二:RepositorySchema(可推导,也可显式声明)
64
+
65
+ **作用**:聚合粒度的存储入口,把"哪些表一起查、怎么拼成聚合"的知识从 service flow 下沉到仓储。调用方只面对领域概念(Order),不面对表。
66
+
67
+ ```ts
68
+ defineRepository({
69
+ name: 'OrderRepository',
70
+ aggregate: orderAggregate, // 绑定聚合
71
+ // 内部如何落到 DAO 由 generator 按 aggregate 结构自动展开:
72
+ // save(order) = tx { ordersDao.upsert + orderItemsDao 级联 }
73
+ // findByOrderNo() = ordersDao.get + orderItemsDao 按 order_no 查
74
+ });
75
+ ```
76
+
77
+ **聚合内 join 下沉、聚合间禁止 join**:
78
+
79
+ - 聚合内:加载整个 Order(根 + 明细 + 地址)由 Repository 内部完成,调用方一行,join 知识声明在 `members[].via`;
80
+ - 聚合间:只按 ID 引用,跨聚合 join 是 DDD 禁止的;展示商品名这类信息走读模型 / 查询服务(CQRS 的 Q 侧),或应用服务分步查 + 内存组装。
81
+
82
+ **分层对照**:
83
+
84
+ | 层 | 操作单元 | 一次操作覆盖 | dsl |
85
+ |----|---------|------------|-----|
86
+ | Service | 用例 | 跨多个聚合/服务 | service_schema ✅ |
87
+ | Repository | **聚合** | orders + order_items 一个事务 | **本扩展** |
88
+ | DAO | **表** | 单表一条 SQL | dao_schema ✅ |
89
+
90
+ ## 4. 扩展三:引入支持 DDD 的 TS 库(选型,待决策)
91
+
92
+ dsl 是**声明期**(`defineAggregate` 声明结构),引入的库是**运行期**(代码跑起来时持久化/发事件)。两者互补,不冲突——声明可翻译为运行期库的配置。
93
+
94
+ | 库 | 类型 | 聚合能力 | 与本扩展关系 |
95
+ |----|------|---------|-------------|
96
+ | **MikroORM** | ORM | Entity / Repository / Unit of Work / Identity Map / cascade persist | **首选参考**:`@OneToMany(cascade, orphanRemoval)` + `em.persist(order)` 就是"orders + order_items 同事务整体落库"的标准实现;AggregateSchema 声明可翻译成它的映射配置 |
97
+ | **Remesh** | DDD 框架 | CQRS + 领域事件 + Command/Query 分离 | 领域事件 / CQRS 参考 |
98
+ | **Emmett** | 事件溯源 | 聚合状态由事件重建 | 事件溯源聚合参考 |
99
+ | TypeORM | ORM | Repository + cascade,无 UoW / Identity Map | 弱支持,不推荐 |
100
+ | Prisma / Drizzle | 查询构建器 | 不支持聚合 | 需手工包 Repository |
101
+
102
+ **建议**:运行时持久化参考/选用 **MikroORM**(聚合持久化设计最完整);领域事件参考 **Remesh**。dsl 的 `AggregateSchema` 声明层本身无现成开源,属本项目的增量设计空间。
103
+
104
+ ## 5. 落地步骤(待办,未开工)
105
+
106
+ 1. `AggregateSchema` 类型 + `defineAggregate` + 定义期校验(root 必须在其表内、via 外键存在、成员表不能是其他聚合的 root 等);
107
+ 2. `RepositorySchema` 类型 + `defineRepository`(或从 aggregate 自动推导生成);
108
+ 3. `gen` 生成 Repository 代码:加载/保存/删除三套骨架 + 同事务包装(`@Trans()` 从声明推导);
109
+ 4. `lint` 聚合边界检查:聚合外禁止直接改成员表、聚合间禁止跨表引用;
110
+ 5. 运行期库选型落地(MikroORM 或保持 DAO 同事务包装)。