@pylonts/dsl 1.1.6 → 1.1.12
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.
- package/README.md +4 -0
- package/dist/action.d.ts +32 -0
- package/dist/action.js +14 -0
- package/dist/aggregate.d.ts +38 -0
- package/dist/aggregate.js +46 -0
- package/dist/business-flow.d.ts +9 -0
- package/dist/business-flow.js +72 -0
- package/dist/controller.d.ts +17 -9
- package/dist/controller.js +8 -2
- package/dist/convert.d.ts +28 -10
- package/dist/convert.js +16 -5
- package/dist/curd.d.ts +7 -10
- package/dist/curd.js +3 -1
- package/dist/dao.d.ts +81 -53
- package/dist/dao.js +291 -12
- package/dist/db.d.ts +6 -0
- package/dist/db.js +10 -0
- package/dist/domain-event.d.ts +48 -0
- package/dist/domain-event.js +24 -0
- package/dist/dsl.d.ts +17 -2
- package/dist/dsl.js +7 -0
- package/dist/dto.d.ts +6 -4
- package/dist/dto.js +5 -4
- package/dist/entity.d.ts +29 -0
- package/dist/entity.js +13 -0
- package/dist/exception.d.ts +9 -3
- package/dist/exception.js +25 -1
- package/dist/expr.d.ts +45 -0
- package/dist/expr.js +32 -0
- package/dist/filter.d.ts +45 -0
- package/dist/filter.js +21 -0
- package/dist/flow-script.d.ts +108 -0
- package/dist/flow-script.js +505 -0
- package/dist/flow.d.ts +294 -17
- package/dist/flow.js +803 -18
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/mermaid-driver.js +264 -24
- package/dist/mysql-driver.js +3 -0
- package/dist/project.d.ts +10 -6
- package/dist/project.js +35 -4
- package/dist/repository.d.ts +26 -0
- package/dist/repository.js +8 -0
- package/dist/service.d.ts +14 -2
- package/dist/service.js +49 -0
- package/dist/third-service.d.ts +5 -0
- package/dist/third-service.js +1 -0
- package/dist/typebox-driver.js +4 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +4 -0
- package/docs/aggregate.md +110 -0
- package/docs/curd.md +146 -111
- package/docs/dao-generation.md +478 -0
- package/docs/ddd-principles.md +75 -0
- package/docs/domain-event.md +137 -0
- package/docs/keyword-matcher.md +182 -0
- package/docs/project.md +17 -9
- package/docs/token.md +327 -0
- package/docs/trans-reentrant.md +85 -0
- package/package.json +25 -6
- package/src/action.ts +51 -10
- package/src/aggregate.ts +104 -0
- package/src/business-flow.ts +80 -0
- package/src/controller.ts +25 -11
- package/src/convert.ts +51 -15
- package/src/curd.ts +12 -6
- package/src/dao.ts +377 -63
- package/src/db.ts +13 -0
- package/src/domain-event.ts +74 -0
- package/src/dsl.ts +23 -2
- package/src/dto.ts +9 -6
- package/src/entity.ts +43 -0
- package/src/exception.ts +30 -5
- package/src/expr.ts +65 -0
- package/src/filter.ts +70 -0
- package/src/flow-script.ts +696 -0
- package/src/flow.ts +1129 -46
- package/src/index.ts +6 -2
- package/src/mermaid-driver.ts +256 -29
- package/src/mysql-driver.ts +3 -0
- package/src/project.ts +138 -97
- package/src/repository.ts +35 -0
- package/src/service.ts +68 -3
- package/src/third-service.ts +6 -0
- package/src/typebox-driver.ts +4 -0
- package/src/utils.ts +13 -2
- package/src/endpoint.ts +0 -18
- 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';
|
package/src/mermaid-driver.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { FlowEdge, FlowNode, FlowSchema } from './flow.js';
|
|
2
|
-
import
|
|
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.
|
|
10
|
-
//
|
|
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, '|');
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export function renderFlowMermaid(schema: FlowSchema): string {
|
|
17
24
|
const lines: string[] = ['flowchart TD'];
|
|
18
|
-
const ids = new Map<
|
|
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<
|
|
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<
|
|
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
|
|
35
|
-
lines.push(` subgraph ${id}["${escapeLabel(
|
|
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
|
-
|
|
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
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
70
|
-
|
|
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
|
package/src/mysql-driver.ts
CHANGED
|
@@ -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,139 @@
|
|
|
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.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
// Directory convention: the instance dir equals its name ('api/' == 'api').
|
|
64
|
+
// One concept, one spelling — no separate dir/name pairs to keep in sync.
|
|
65
|
+
function normalizedDir(dir: string): string {
|
|
66
|
+
return dir.replace(/[\\/]+$/, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function checkDirMatchesName(project: string, kind: string, name: string, dir: string): void {
|
|
70
|
+
if (normalizedDir(dir) !== name) {
|
|
71
|
+
throw new Error(`project ${project}: ${kind} '${name}' dir must equal its name (got '${dir}')`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Defines the project topology. FrontAppSchema instances are shared value objects:
|
|
77
|
+
* api.apps references the same instances from project.apps, so an app served
|
|
78
|
+
* by multiple APIs is defined once and referenced many times.
|
|
79
|
+
*
|
|
80
|
+
* Runtime-validates app type whitelist, unique names, api.apps reference
|
|
81
|
+
* integrity (same style as defineTable/defineCurd), plus two naming
|
|
82
|
+
* conventions: every app/api/thirdApi dir equals its name ('api/' == 'api'),
|
|
83
|
+
* and the first api must be named exactly 'api' (prefixed names like
|
|
84
|
+
* 'xx-api' are only allowed from the second api on).
|
|
85
|
+
*/
|
|
86
|
+
export function defineProject(
|
|
87
|
+
name: string,
|
|
88
|
+
schema: {
|
|
89
|
+
description?: string;
|
|
90
|
+
apps: FrontAppSchema[];
|
|
91
|
+
apis: ProjectApiSchema[];
|
|
92
|
+
thirdApis?: ThirdApiSchema[];
|
|
93
|
+
},
|
|
94
|
+
): ProjectSchema {
|
|
95
|
+
const project: ProjectSchema = { name, ...schema, thirdApis: schema.thirdApis ?? [] };
|
|
96
|
+
|
|
97
|
+
const appNames = new Set<string>();
|
|
98
|
+
for (const app of project.apps) {
|
|
99
|
+
if (!app.name) throw new Error(`project ${name}: app name is required`);
|
|
100
|
+
checkInstanceName(name, 'app', app.name);
|
|
101
|
+
if (appNames.has(app.name)) throw new Error(`project ${name}: duplicate app name '${app.name}'`);
|
|
102
|
+
appNames.add(app.name);
|
|
103
|
+
if (app.type !== 'admin' && app.type !== 'wxmini' && app.type !== 'mobile') {
|
|
104
|
+
throw new Error(`project ${name}: app '${app.name}' must be type 'admin', 'wxmini' or 'mobile' (got '${app.type}')`);
|
|
105
|
+
}
|
|
106
|
+
if (!app.dir) throw new Error(`project ${name}: app '${app.name}' dir is required`);
|
|
107
|
+
checkDirMatchesName(name, 'app', app.name, app.dir);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const apiNames = new Set<string>();
|
|
111
|
+
for (const api of project.apis) {
|
|
112
|
+
if (!api.name) throw new Error(`project ${name}: api name is required`);
|
|
113
|
+
checkInstanceName(name, 'api', api.name);
|
|
114
|
+
if (apiNames.has(api.name)) throw new Error(`project ${name}: duplicate api name '${api.name}'`);
|
|
115
|
+
apiNames.add(api.name);
|
|
116
|
+
if (!api.dir) throw new Error(`project ${name}: api '${api.name}' dir is required`);
|
|
117
|
+
checkDirMatchesName(name, 'api', api.name, api.dir);
|
|
118
|
+
for (const ref of api.apps) {
|
|
119
|
+
if (!project.apps.includes(ref)) {
|
|
120
|
+
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)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (project.apis.length > 0 && project.apis[0].name !== 'api') {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`project ${name}: first api must be named 'api' (got '${project.apis[0].name}'); prefixed names like 'xx-api' are only allowed from the second api on`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const third of project.thirdApis) {
|
|
132
|
+
if (!third.name) throw new Error(`project ${name}: thirdApi name is required`);
|
|
133
|
+
checkInstanceName(name, 'thirdApi', third.name);
|
|
134
|
+
if (!third.dir) throw new Error(`project ${name}: thirdApi '${third.name}' dir is required`);
|
|
135
|
+
checkDirMatchesName(name, 'thirdApi', third.name, third.dir);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return project;
|
|
98
139
|
}
|
|
@@ -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
|
+
}
|