@pylonts/dsl 1.1.6 → 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 (86) 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 +17 -9
  9. package/dist/controller.js +8 -2
  10. package/dist/convert.d.ts +28 -10
  11. package/dist/convert.js +16 -5
  12. package/dist/curd.d.ts +7 -10
  13. package/dist/curd.js +3 -1
  14. package/dist/dao.d.ts +81 -53
  15. package/dist/dao.js +291 -12
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +10 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +17 -2
  21. package/dist/dsl.js +7 -0
  22. package/dist/dto.d.ts +6 -4
  23. package/dist/dto.js +5 -4
  24. package/dist/entity.d.ts +29 -0
  25. package/dist/entity.js +13 -0
  26. package/dist/exception.d.ts +9 -3
  27. package/dist/exception.js +25 -1
  28. package/dist/expr.d.ts +45 -0
  29. package/dist/expr.js +32 -0
  30. package/dist/filter.d.ts +45 -0
  31. package/dist/filter.js +21 -0
  32. package/dist/flow-script.d.ts +108 -0
  33. package/dist/flow-script.js +505 -0
  34. package/dist/flow.d.ts +294 -17
  35. package/dist/flow.js +803 -18
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.js +6 -2
  38. package/dist/mermaid-driver.js +264 -24
  39. package/dist/mysql-driver.js +3 -0
  40. package/dist/project.d.ts +5 -4
  41. package/dist/project.js +14 -2
  42. package/dist/repository.d.ts +26 -0
  43. package/dist/repository.js +8 -0
  44. package/dist/service.d.ts +14 -2
  45. package/dist/service.js +49 -0
  46. package/dist/third-service.d.ts +5 -0
  47. package/dist/third-service.js +1 -0
  48. package/dist/typebox-driver.js +4 -0
  49. package/dist/utils.d.ts +9 -2
  50. package/dist/utils.js +4 -0
  51. package/docs/aggregate.md +110 -0
  52. package/docs/dao-generation.md +478 -0
  53. package/docs/ddd-principles.md +75 -0
  54. package/docs/domain-event.md +137 -0
  55. package/docs/keyword-matcher.md +182 -0
  56. package/docs/token.md +327 -0
  57. package/docs/trans-reentrant.md +85 -0
  58. package/package.json +25 -6
  59. package/src/action.ts +51 -10
  60. package/src/aggregate.ts +104 -0
  61. package/src/business-flow.ts +80 -0
  62. package/src/controller.ts +25 -11
  63. package/src/convert.ts +51 -15
  64. package/src/curd.ts +12 -6
  65. package/src/dao.ts +377 -63
  66. package/src/db.ts +13 -0
  67. package/src/domain-event.ts +74 -0
  68. package/src/dsl.ts +23 -2
  69. package/src/dto.ts +9 -6
  70. package/src/entity.ts +43 -0
  71. package/src/exception.ts +30 -5
  72. package/src/expr.ts +65 -0
  73. package/src/filter.ts +70 -0
  74. package/src/flow-script.ts +696 -0
  75. package/src/flow.ts +1129 -46
  76. package/src/index.ts +6 -2
  77. package/src/mermaid-driver.ts +256 -29
  78. package/src/mysql-driver.ts +3 -0
  79. package/src/project.ts +114 -97
  80. package/src/repository.ts +35 -0
  81. package/src/service.ts +68 -3
  82. package/src/third-service.ts +6 -0
  83. package/src/typebox-driver.ts +4 -0
  84. package/src/utils.ts +13 -2
  85. package/src/endpoint.ts +0 -18
  86. package/src/provider.ts +0 -68
package/dist/index.d.ts CHANGED
@@ -23,15 +23,19 @@ export * from './ref.js';
23
23
  export * from './route.js';
24
24
  export * from './service.js';
25
25
  export * from './controller.js';
26
- export * from './endpoint.js';
27
26
  export * from './dao.js';
27
+ export * from './entity.js';
28
+ export * from './expr.js';
29
+ export * from './filter.js';
30
+ export * from './aggregate.js';
31
+ export * from './repository.js';
32
+ export * from './domain-event.js';
28
33
  export * from './third-service.js';
29
34
  export * from './field-rule.js';
30
35
  export * from './exception.js';
31
36
  export * from './page.js';
32
37
  export * from './curd.js';
33
38
  export * from './page-flow.js';
34
- export * from './provider.js';
35
39
  export * from './page-def.js';
36
40
  export * from './mermaid-driver.js';
37
41
  export * from './navigation.js';
package/dist/index.js CHANGED
@@ -23,15 +23,19 @@ export * from './ref.js';
23
23
  export * from './route.js';
24
24
  export * from './service.js';
25
25
  export * from './controller.js';
26
- export * from './endpoint.js';
27
26
  export * from './dao.js';
27
+ export * from './entity.js';
28
+ export * from './expr.js';
29
+ export * from './filter.js';
30
+ export * from './aggregate.js';
31
+ export * from './repository.js';
32
+ export * from './domain-event.js';
28
33
  export * from './third-service.js';
29
34
  export * from './field-rule.js';
30
35
  export * from './exception.js';
31
36
  export * from './page.js';
32
37
  export * from './curd.js';
33
38
  export * from './page-flow.js';
34
- export * from './provider.js';
35
39
  export * from './page-def.js';
36
40
  export * from './mermaid-driver.js';
37
41
  export * from './navigation.js';
@@ -1,10 +1,17 @@
1
+ import { isCall, methodOf } from './flow.js';
1
2
  // Mermaid driver: converts a FlowSchema into a Mermaid flowchart (TD).
2
3
  // Node ids are hierarchical (n0, n0_0, n0_0_0, ...) so nested sub-flows stay
3
4
  // globally unique. A node with a sub-flow renders as a subgraph block whose
4
- // internals are rendered recursively. ok edges render as -->, conditional
5
- // edges render as -->|"WHEN"|.
5
+ // internals are rendered recursively. A TryNode renders as a subgraph for its
6
+ // body plus one per catch handler (and finally): body exception ends route to
7
+ // handlers as dashed `catch X` edges, fall-through paths (body return end and
8
+ // handler return ends, through finally when present) continue via the
9
+ // TryNode's outgoing edges, and handler exception ends rethrow via the
10
+ // TryNode's typed throws edges. Guard nodes render as diamonds, FlowEnd nodes
11
+ // as stadium shapes. Normal edges render as -->, conditional edges as
12
+ // -->|"WHEN"|, typed throws and catch routes as dashed edges.
6
13
  function escapeLabel(s) {
7
- return s.replace(/"/g, '\\"').replace(/\n/g, '<br/>');
14
+ return s.replace(/"/g, '\\"').replace(/\n/g, '<br/>').replace(/\|/g, '&#124;');
8
15
  }
9
16
  export function renderFlowMermaid(schema) {
10
17
  const lines = ['flowchart TD'];
@@ -24,46 +31,279 @@ function renderFlow(schema, prefix, lines, ids) {
24
31
  outgoing.get(e.start).push(e);
25
32
  for (const n of schema.nodes) {
26
33
  const id = ids.get(n);
27
- if (n.flow) {
28
- lines.push(` subgraph ${id}["${escapeLabel(renderNodeLabel(n))}"]`);
34
+ if (isTryNode(n)) {
35
+ lines.push(` subgraph ${id}["${escapeLabel(n.name)}"]`);
36
+ renderFlow(n.body, `${id}_b`, lines, ids);
37
+ lines.push(' end');
38
+ const handlers = [];
39
+ const handlerIdx = new Map();
40
+ for (const c of n.catches) {
41
+ if (handlerIdx.has(c.handler))
42
+ continue; // one handler may serve many catches
43
+ handlerIdx.set(c.handler, handlers.length);
44
+ handlers.push(c.handler);
45
+ }
46
+ handlers.forEach((h, i) => {
47
+ lines.push(` subgraph ${id}_h${i}["${escapeLabel(h.name)}"]`);
48
+ renderFlow(h, `${id}_h${i}_`, lines, ids);
49
+ lines.push(' end');
50
+ });
51
+ if (n.finally) {
52
+ lines.push(` subgraph ${id}_f["${escapeLabel(n.finally.name)}"]`);
53
+ renderFlow(n.finally, `${id}_f_`, lines, ids);
54
+ lines.push(' end');
55
+ }
56
+ }
57
+ else if (isFlowNode(n) && n.flow) {
58
+ lines.push(` subgraph ${id}["${escapeLabel(renderNodeLabel(n, outgoing.get(n)))}"]`);
29
59
  renderFlow(n.flow, `${id}_`, lines, ids);
30
60
  lines.push(' end');
31
61
  }
32
62
  else {
33
63
  const shape = renderShape(n, schema, outgoing.get(n));
34
- lines.push(` ${id}${shape.open}${escapeLabel(renderNodeLabel(n))}${shape.close}`);
64
+ lines.push(` ${id}${shape.open}${escapeLabel(renderNodeLabel(n, outgoing.get(n)))}${shape.close}`);
35
65
  }
36
66
  }
37
67
  for (const e of schema.edges) {
38
- lines.push(renderEdge(e, ids));
68
+ if (isTryNode(e.start))
69
+ continue; // rendered by renderTryRoutes
70
+ if (isTryNode(e.end)) {
71
+ // entering a TryNode means entering its body
72
+ lines.push(renderEdgeLine(e, ids.get(e.start), ids.get(e.end.body.start)));
73
+ continue;
74
+ }
75
+ lines.push(renderEdgeLine(e, ids.get(e.start), ids.get(e.end)));
76
+ }
77
+ // guard checks: implicit branch edges of the guard itself
78
+ for (const n of schema.nodes) {
79
+ if (!isGuard(n))
80
+ continue;
81
+ const id = ids.get(n);
82
+ for (const c of n.checks) {
83
+ if (c.return) {
84
+ lines.push(` ${id} -->|"${escapeLabel(c.when)}"| ${ids.get(schema.returnEnd)}`);
85
+ }
86
+ else if (c.exception) {
87
+ const t = findExceptionEnd(schema, c.exception.name);
88
+ lines.push(` ${id} -.->|"${escapeLabel(c.when)} · throw ${escapeLabel(c.exception.name)}"| ${ids.get(t)}`);
89
+ }
90
+ }
91
+ }
92
+ // ifNode cases: implicit branch edges of the decision node
93
+ const branchTarget = (t) => (isTryNode(t) ? t.body.start : t);
94
+ for (const n of schema.nodes) {
95
+ if (!isIfNode(n))
96
+ continue;
97
+ const id = ids.get(n);
98
+ for (const c of n.cases) {
99
+ lines.push(` ${id} -->|"${escapeLabel(c.when)}"| ${ids.get(branchTarget(c.to))}`);
100
+ }
101
+ lines.push(` ${id} -->|"else"| ${ids.get(branchTarget(n.else))}`);
102
+ }
103
+ // TryNode internal routes
104
+ for (const n of schema.nodes) {
105
+ if (isTryNode(n))
106
+ renderTryRoutes(n, schema, ids, lines);
107
+ }
108
+ }
109
+ /** Routes of a TryNode: catch edges, fall-through continuation (through
110
+ * finally when present), and handler rethrows (also through finally). */
111
+ function renderTryRoutes(n, schema, ids, lines) {
112
+ const next = schema.edges.filter((e) => e.start === n && e.throws === undefined && e.exception !== true);
113
+ const rethrows = schema.edges.filter((e) => e.start === n && e.throws !== undefined);
114
+ for (const c of n.catches) {
115
+ const end = findExceptionEnd(n.body, c.exception.name);
116
+ lines.push(` ${ids.get(end)} -.->|"catch ${escapeLabel(c.exception.name)}"| ${ids.get(c.handler.start)}`);
117
+ }
118
+ const continueFrom = (sourceId) => {
119
+ if (n.finally) {
120
+ lines.push(` ${sourceId} --> ${ids.get(n.finally.start)}`);
121
+ return;
122
+ }
123
+ for (const e of next) {
124
+ lines.push(renderEdgeLine(e, sourceId, ids.get(e.end)));
125
+ }
126
+ };
127
+ continueFrom(ids.get(n.body.returnEnd));
128
+ const doneHandlers = new Set();
129
+ for (const c of n.catches) {
130
+ if (doneHandlers.has(c.handler))
131
+ continue; // one handler may serve many catches
132
+ doneHandlers.add(c.handler);
133
+ // A handler whose every path throws has no fall-through — its return end
134
+ // is never targeted and stays out of the handler's node list.
135
+ const handlerReturn = ids.get(c.handler.returnEnd);
136
+ if (handlerReturn !== undefined)
137
+ continueFrom(handlerReturn);
138
+ }
139
+ if (n.finally) {
140
+ for (const e of next) {
141
+ lines.push(renderEdgeLine(e, ids.get(n.finally.returnEnd), ids.get(e.end)));
142
+ }
143
+ }
144
+ const rethrown = new Map();
145
+ for (const e of rethrows) {
146
+ for (const c of n.catches) {
147
+ let names = rethrown.get(c.handler);
148
+ if (!names) {
149
+ names = new Set();
150
+ rethrown.set(c.handler, names);
151
+ }
152
+ if (names.has(e.throws.name))
153
+ continue; // one handler may serve many catches
154
+ names.add(e.throws.name);
155
+ const end = findExceptionEnd(c.handler, e.throws.name);
156
+ if (!end)
157
+ continue;
158
+ if (n.finally) {
159
+ lines.push(` ${ids.get(end)} -.->|"rethrow ${escapeLabel(e.throws.name)}"| ${ids.get(n.finally.start)}`);
160
+ lines.push(` ${ids.get(n.finally.returnEnd)} -.->|"rethrow ${escapeLabel(e.throws.name)}"| ${ids.get(e.end)}`);
161
+ }
162
+ else {
163
+ lines.push(` ${ids.get(end)} -.->|"rethrow ${escapeLabel(e.throws.name)}"| ${ids.get(e.end)}`);
164
+ }
165
+ }
39
166
  }
40
167
  }
41
- /** Node label: name on the first line, method references on the second. */
42
- function renderNodeLabel(n) {
43
- if (n.methods === undefined || n.methods.length === 0)
168
+ /** The exception end of a flow carrying the given exception name. */
169
+ function findExceptionEnd(flow, exceptionName) {
170
+ const end = flow.nodes.find((n) => isEnd(n) && n.type === 'exception' && n.exception?.name === exceptionName);
171
+ if (!end) {
172
+ throw new Error(`flow ${flow.name}: no exception end for ${exceptionName}`);
173
+ }
174
+ return end;
175
+ }
176
+ /** Node label: name on the first line, method references (including utils
177
+ * predicates of guard checks) on the second, and the data line (slots
178
+ * consumed `r:` / produced `w:`) last. */
179
+ function renderNodeLabel(n, outgoing) {
180
+ if (isEnd(n) || isTryNode(n))
44
181
  return n.name;
45
- return `${n.name}\n${n.methods.map((m) => renderMethodRef(m)).join(' | ')}`;
182
+ const lines = [n.name];
183
+ if (isFlowNode(n) && n.publish) {
184
+ const payload = n.publish.payload ? ` (${n.publish.payload.name})` : '';
185
+ lines.push(`publish ${n.publish.event.name}${payload}`);
186
+ }
187
+ const refs = [];
188
+ if (!isIfNode(n)) {
189
+ refs.push(...(n.methods ?? []));
190
+ if (isGuard(n)) {
191
+ for (const c of n.checks) {
192
+ if (c.check !== undefined && isCall(c.check))
193
+ refs.push(c.check);
194
+ }
195
+ }
196
+ }
197
+ if (refs.length > 0) {
198
+ const rendered = refs.map((m) => renderMethodRef(methodOf(m))).join(' | ');
199
+ // Script-compiled nodes carry the full method name themselves.
200
+ if (rendered !== n.name)
201
+ lines.push(rendered);
202
+ }
203
+ const data = renderDataLine(n, outgoing);
204
+ if (data !== '')
205
+ lines.push(data);
206
+ return lines.join('\n');
46
207
  }
47
- /** Display form: owner.name for descriptors and container methods, bare name for converts. */
208
+ /** Data line: `r:` lists slots the node consumes (reads, call args, decision
209
+ * condition slots, and branch-edge conditions decided here), `w:` lists
210
+ * slots it produces (writes and call results). */
211
+ function renderDataLine(n, outgoing) {
212
+ const reads = new Set();
213
+ const writes = new Set();
214
+ const addCondition = (c) => {
215
+ if (c === undefined)
216
+ return;
217
+ if (!isCall(c)) {
218
+ reads.add(c.field.slot.name);
219
+ return;
220
+ }
221
+ for (const t of c.args ?? [])
222
+ reads.add(t.name);
223
+ };
224
+ if (isGuard(n)) {
225
+ for (const c of n.checks) {
226
+ for (const t of c.reads ?? [])
227
+ reads.add(t.name);
228
+ addCondition(c.check);
229
+ }
230
+ }
231
+ else if (isIfNode(n)) {
232
+ for (const c of n.cases)
233
+ addCondition(c.check);
234
+ }
235
+ else {
236
+ for (const t of n.reads ?? [])
237
+ reads.add(t.name);
238
+ for (const t of n.writes ?? [])
239
+ writes.add(t.name);
240
+ if (isFlowNode(n) && n.publish?.payload)
241
+ reads.add(n.publish.payload.name);
242
+ }
243
+ for (const e of outgoing)
244
+ addCondition(e.check);
245
+ if (!isIfNode(n)) {
246
+ for (const ref of n.methods ?? []) {
247
+ if (!isCall(ref))
248
+ continue;
249
+ for (const t of ref.args ?? [])
250
+ reads.add(t.name);
251
+ if (ref.result)
252
+ writes.add(ref.result.name);
253
+ }
254
+ }
255
+ const parts = [];
256
+ if (reads.size > 0)
257
+ parts.push(`r:${[...reads].join(',')}`);
258
+ if (writes.size > 0)
259
+ parts.push(`w:${[...writes].join(',')}`);
260
+ return parts.join(' ');
261
+ }
262
+ /** Display form: owner.name for descriptors, schema.name for container methods. */
48
263
  function renderMethodRef(m) {
49
264
  if ('owner' in m)
50
265
  return `${m.owner}.${m.name}`;
51
- if ('schema' in m)
52
- return `${m.schema.name}.${m.name}`;
53
- return m.name;
266
+ return `${m.schema.name}.${m.name}`;
54
267
  }
55
- /** Shape derived from topology: start and terminals rounded, branching nodes diamond, else rect.
56
- * Exception edges do not count as branches. */
268
+ /** Shape derived from topology: guards are diamonds, ends rounded, other
269
+ * branching nodes diamonds, else rect. Exception edges (typed or not) do not
270
+ * count as branches. The start keeps its rounded shape unless it branches. */
57
271
  function renderShape(n, schema, outgoing) {
58
- if (n === schema.start || outgoing.length === 0)
272
+ if (isGuard(n))
273
+ return { open: '{"', close: '"}' };
274
+ if (isIfNode(n))
275
+ return { open: '{"', close: '"}' };
276
+ if (isEnd(n) || outgoing.length === 0)
277
+ return { open: '(["', close: '"])' };
278
+ const normal = outgoing.filter((e) => e.exception !== true && e.throws === undefined).length;
279
+ if (normal >= 2)
280
+ return { open: '{"', close: '"}' };
281
+ if (n === schema.start)
59
282
  return { open: '(["', close: '"])' };
60
- const normal = outgoing.filter((e) => e.exception !== true).length;
61
- return normal >= 2 ? { open: '{"', close: '"}' } : { open: '["', close: '"]' };
283
+ return { open: '["', close: '"]' };
284
+ }
285
+ function renderEdgeLine(e, startId, endId) {
286
+ const isException = e.exception === true || e.throws !== undefined;
287
+ const arrow = isException ? '-.->' : '-->';
288
+ const throwLabel = e.throws ? `throw ${escapeLabel(e.throws.name)}` : '';
289
+ const labelParts = [e.when ? escapeLabel(e.when) : '', throwLabel].filter((s) => s !== '');
290
+ const label = labelParts.length > 0 ? `|"${labelParts.join(' · ')}"|` : '';
291
+ return ` ${startId} ${arrow}${label} ${endId}`;
292
+ }
293
+ function isEnd(n) {
294
+ return 'type' in n && (n.type === 'return' || n.type === 'exception');
295
+ }
296
+ function isFlowNode(n) {
297
+ return !('type' in n);
298
+ }
299
+ function isGuard(n) {
300
+ return 'type' in n && n.type === 'guard';
301
+ }
302
+ function isTryNode(n) {
303
+ return 'type' in n && n.type === 'try';
62
304
  }
63
- function renderEdge(e, ids) {
64
- const arrow = e.exception === true ? '-.->' : '-->';
65
- const label = e.when ? `|"${escapeLabel(e.when)}"|` : '';
66
- return ` ${ids.get(e.start)} ${arrow}${label} ${ids.get(e.end)}`;
305
+ function isIfNode(n) {
306
+ return 'type' in n && n.type === 'if';
67
307
  }
68
308
  // Page-driven flow renderer: groups pages by their app into swimlane
69
309
  // subgraphs, then renders edges across the whole flow.
@@ -37,6 +37,9 @@ function columnType(field) {
37
37
  case 'object':
38
38
  // Nested fields are wire-format only (third-party messages); table columns cannot nest.
39
39
  throw new Error(`field ${field.name} (${field.type}): nested fields are not supported on table columns`);
40
+ case 'aggregate':
41
+ // Aggregate fields are query outputs, never table columns.
42
+ throw new Error(`field ${field.name} (aggregate): aggregate fields are not supported on table columns`);
40
43
  }
41
44
  }
42
45
  function renderDefault(field) {
package/dist/project.d.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  import { SchemaBase } from './dsl.js';
2
2
  import type { TableSchema } from './db.js';
3
3
  /** Frontend form factor. Closed enum, extend when new form factors appear. */
4
- export type FrontType = 'admin' | 'wxmini';
4
+ export type FrontType = 'admin' | 'wxmini' | 'mobile';
5
5
  /** A frontend application (e.g. admin console, wechat mini program). */
6
6
  export interface FrontAppSchema extends SchemaBase {
7
7
  type: FrontType;
8
8
  /** Source directory relative to project root, e.g. 'web-admin/'. */
9
9
  dir: string;
10
10
  /**
11
- * Tenant table for this app. When set, all tables with a foreign key
12
- * pointing to this table get automatic tenant scoping: the tenant PK
13
- * value is injected from `user.id` into all curd operations.
11
+ * Tenant table for this app. The tenant column of a business table is
12
+ * deterministic: `{tenant.phrase}_{tenant.pk}` (e.g. shop with pk id
13
+ * `shop_id`). Tables carrying that column get automatic tenant scoping;
14
+ * tables without it are global tables (e.g. system config) — both valid.
14
15
  */
15
16
  tenant?: TableSchema;
16
17
  }
package/dist/project.js CHANGED
@@ -1,3 +1,12 @@
1
+ // Instance naming: lowercase letters/digits/dashes only. Underscores belong
2
+ // to table names; the instance export symbol in project.config.ts must equal
3
+ // the kebab-camel of the name, so 'admin_api' cannot map to a valid symbol.
4
+ const INSTANCE_NAME_RE = /^[a-z][a-z0-9-]*$/;
5
+ function checkInstanceName(project, kind, name) {
6
+ if (!INSTANCE_NAME_RE.test(name)) {
7
+ throw new Error(`project ${project}: ${kind} name '${name}' must match ${INSTANCE_NAME_RE} (lowercase letters/digits/dashes; underscores are table-only)`);
8
+ }
9
+ }
1
10
  /**
2
11
  * Defines the project topology. FrontAppSchema instances are shared value objects:
3
12
  * api.apps references the same instances from project.apps, so an app served
@@ -12,11 +21,12 @@ export function defineProject(name, schema) {
12
21
  for (const app of project.apps) {
13
22
  if (!app.name)
14
23
  throw new Error(`project ${name}: app name is required`);
24
+ checkInstanceName(name, 'app', app.name);
15
25
  if (appNames.has(app.name))
16
26
  throw new Error(`project ${name}: duplicate app name '${app.name}'`);
17
27
  appNames.add(app.name);
18
- if (app.type !== 'admin' && app.type !== 'wxmini') {
19
- throw new Error(`project ${name}: app '${app.name}' must be type 'admin' or 'wxmini' (got '${app.type}')`);
28
+ if (app.type !== 'admin' && app.type !== 'wxmini' && app.type !== 'mobile') {
29
+ throw new Error(`project ${name}: app '${app.name}' must be type 'admin', 'wxmini' or 'mobile' (got '${app.type}')`);
20
30
  }
21
31
  if (!app.dir)
22
32
  throw new Error(`project ${name}: app '${app.name}' dir is required`);
@@ -25,6 +35,7 @@ export function defineProject(name, schema) {
25
35
  for (const api of project.apis) {
26
36
  if (!api.name)
27
37
  throw new Error(`project ${name}: api name is required`);
38
+ checkInstanceName(name, 'api', api.name);
28
39
  if (apiNames.has(api.name))
29
40
  throw new Error(`project ${name}: duplicate api name '${api.name}'`);
30
41
  apiNames.add(api.name);
@@ -39,6 +50,7 @@ export function defineProject(name, schema) {
39
50
  for (const third of project.thirdApis) {
40
51
  if (!third.name)
41
52
  throw new Error(`project ${name}: thirdApi name is required`);
53
+ checkInstanceName(name, 'thirdApi', third.name);
42
54
  if (!third.dir)
43
55
  throw new Error(`project ${name}: thirdApi '${third.name}' dir is required`);
44
56
  }
@@ -0,0 +1,26 @@
1
+ import { SchemaBase } from './dsl.js';
2
+ import type { DomainAggregate } from './aggregate.js';
3
+ /**
4
+ * Aggregate-grained storage entry. The repository binds one aggregate and
5
+ * exposes the three fixed operation skeletons (load / save / delete) that the
6
+ * generator expands from the aggregate structure:
7
+ *
8
+ * save(order) = tx { rootDao.upsert + memberDao cascade by via FK }
9
+ * load(id) = rootDao.get + memberDao by via FK
10
+ * delete(id) = tx { memberDao delete + rootDao delete }
11
+ *
12
+ * Callers face the domain concept (Order), never the tables. DAO stays
13
+ * single-table; Repository is the multi-table (aggregate) unit; Service is the
14
+ * use-case unit. Inter-aggregate joins are prohibited — repositories only
15
+ * reference each other by root ID.
16
+ */
17
+ export interface RepositorySchema extends SchemaBase {
18
+ type: 'repository';
19
+ /** The aggregate this repository persists (shared instance). */
20
+ aggregate: DomainAggregate;
21
+ }
22
+ export declare function defineRepository(options: {
23
+ name: string;
24
+ aggregate: DomainAggregate;
25
+ description?: string;
26
+ }): RepositorySchema;
@@ -0,0 +1,8 @@
1
+ export function defineRepository(options) {
2
+ return {
3
+ type: 'repository',
4
+ name: options.name,
5
+ description: options.description,
6
+ aggregate: options.aggregate,
7
+ };
8
+ }
package/dist/service.d.ts CHANGED
@@ -1,9 +1,15 @@
1
- import { SchemaBase } from './dsl.js';
2
- import { FrontAppSchema } from './project.js';
1
+ import type { SchemaBase } from './dsl.js';
2
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
3
  import type { DtoMessage } from './dto.js';
4
+ import type { ExceptionSchema } from './exception.js';
5
+ import type { FlowSchema } from './flow.js';
4
6
  /** A backend service serving exactly one frontend app (1:1 module). */
5
7
  export interface ServiceSchema extends SchemaBase {
6
8
  type: 'service';
9
+ /** The backend api module this service belongs to (shared instance from
10
+ * project.config.ts apis). Services are always backend-side, so storage is
11
+ * service_schema/{api.name}/{app.name}/service/. */
12
+ api: ProjectApiSchema;
7
13
  /** The frontend app this service serves (shared instance from project.config). */
8
14
  app: FrontAppSchema;
9
15
  /** Methods keyed by name — the map key is written back as the method name. */
@@ -13,6 +19,7 @@ export interface ServiceSchema extends SchemaBase {
13
19
  export type ServiceMethodDef = Omit<ServiceMethodSchema, 'type' | 'schema' | 'name'>;
14
20
  export declare function defineService(options: {
15
21
  name: string;
22
+ api: ProjectApiSchema;
16
23
  app: FrontAppSchema;
17
24
  methods: Record<string, ServiceMethodDef>;
18
25
  description?: string;
@@ -23,4 +30,9 @@ export interface ServiceMethodSchema extends SchemaBase {
23
30
  schema: ServiceSchema;
24
31
  args: DtoMessage;
25
32
  results: DtoMessage;
33
+ /** Exceptions this method may throw (e.g. IOException, CodeException). */
34
+ throws?: ExceptionSchema[];
35
+ /** Implementation flow — the method's logic graph. The method carries the
36
+ * contract (args/results/throws), the flow carries the structure. */
37
+ flow?: FlowSchema;
26
38
  }
package/dist/service.js CHANGED
@@ -1,8 +1,13 @@
1
+ import { exceptionEndNames } from './flow.js';
1
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
+ }
2
6
  const schema = {
3
7
  type: 'service',
4
8
  name: options.name,
5
9
  description: options.description,
10
+ api: options.api,
6
11
  app: options.app,
7
12
  methods: {},
8
13
  };
@@ -10,5 +15,49 @@ export function defineService(options) {
10
15
  const method = options.methods[key];
11
16
  schema.methods[key] = { type: 'method', schema, ...method, name: key };
12
17
  }
18
+ validateFlowBindings(schema);
13
19
  return schema;
14
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
+ }
63
+ }
@@ -1,4 +1,5 @@
1
1
  import { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
2
+ import type { ExceptionSchema } from './exception.js';
2
3
  import type { FieldRuleEnd, FieldRuleSchema } from './field-rule.js';
3
4
  import type { ThirdApiSchema } from './project.js';
4
5
  /** A third-party integration service (e.g. tenpay wechat pay).
@@ -20,6 +21,8 @@ export interface ThirdServiceMethodSchema extends SchemaBase {
20
21
  args: ThirdMethodSchema;
21
22
  /** Output message. */
22
23
  results: ThirdMethodSchema;
24
+ /** Exceptions this method may throw (e.g. IOException, CodeException). */
25
+ throws?: ExceptionSchema[];
23
26
  }
24
27
  /** Field binding to a rule: which end the local wire field stands on.
25
28
  * The ref always stands on the other end — from/to carry no extra information. */
@@ -65,6 +68,8 @@ export interface ThirdServiceMethodDef {
65
68
  args: ThirdMethodDef;
66
69
  /** Output message. */
67
70
  results: ThirdMethodDef;
71
+ /** Exceptions this method may throw (e.g. IOException, CodeException). */
72
+ throws?: ExceptionSchema[];
68
73
  description?: string;
69
74
  }
70
75
  export declare function defineThirdService(options: {
@@ -85,6 +85,7 @@ export function defineThirdService(options) {
85
85
  schema,
86
86
  args: undefined,
87
87
  results: undefined,
88
+ throws: method.throws,
88
89
  };
89
90
  methodSchema.args = defineThirdMethod(method.args);
90
91
  methodSchema.results = defineThirdMethod(method.results);
@@ -62,6 +62,10 @@ function renderBasic(field, pattern, defaultValue, resolver, indent = 0) {
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()';
65
69
  case 'array':
66
70
  return `Type.Array(${renderFieldValue(field.items, indent, resolver)})`;
67
71
  case 'object':
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Field, SchemaBase } from './dsl.js';
2
- import type { FrontAppSchema } from './project.js';
2
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
3
  /** A utility method with a full signature. */
4
4
  export interface UtilsMethodSchema extends SchemaBase {
5
5
  type: 'utilsMethod';
@@ -15,13 +15,20 @@ export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>
15
15
  /** A base utility module (e.g. DateTimeUtils). */
16
16
  export interface UtilsSchema extends SchemaBase {
17
17
  type: 'utils';
18
- /** Optional binding — empty means a shared public module. */
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. */
19
25
  app?: FrontAppSchema;
20
26
  /** Methods keyed by name — the map key is written back as the method name. */
21
27
  methods: Record<string, UtilsMethodSchema>;
22
28
  }
23
29
  export declare function defineUtils(options: {
24
30
  name: string;
31
+ api?: ProjectApiSchema;
25
32
  app?: FrontAppSchema;
26
33
  methods: Record<string, UtilsMethodDef>;
27
34
  description?: string;