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