@pylonts/dsl 1.1.20 → 1.1.22
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 +8 -5
- package/dist/action.d.ts +76 -33
- package/dist/action.js +19 -17
- package/dist/aggregate.d.ts +3 -13
- package/dist/aggregate.js +19 -22
- package/dist/component.d.ts +3 -3
- package/dist/curd.d.ts +2 -2
- package/dist/dto.d.ts +18 -2
- package/dist/dto.js +75 -9
- package/dist/flow-script.js +73 -7
- package/dist/flow.d.ts +20 -3
- package/dist/flow.js +82 -11
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/journey.d.ts +23 -0
- package/dist/journey.js +26 -0
- package/dist/mermaid-driver.js +6 -1
- package/dist/navigation.d.ts +2 -2
- package/dist/page-action.d.ts +39 -0
- package/dist/page-action.js +17 -0
- package/dist/page-def.d.ts +9 -9
- package/dist/page-flow.d.ts +4 -4
- package/dist/popup.d.ts +3 -3
- package/dist/repository.d.ts +2 -2
- package/dist/task.d.ts +20 -0
- package/dist/task.js +21 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +12 -4
- package/docs/aggregate-implementation.md +174 -0
- package/docs/aggregate.md +147 -110
- package/docs/concepts.md +109 -0
- package/docs/dto.md +130 -106
- package/docs/table.md +41 -0
- package/docs/task.md +81 -0
- package/docs/utils.md +19 -12
- package/package.json +1 -1
- package/src/action.ts +87 -52
- package/src/aggregate.ts +94 -103
- package/src/component.ts +3 -3
- package/src/curd.ts +2 -2
- package/src/dto.ts +87 -8
- package/src/flow-script.ts +68 -6
- package/src/flow.ts +99 -17
- package/src/index.ts +3 -0
- package/src/journey.ts +61 -0
- package/src/mermaid-driver.ts +5 -1
- package/src/navigation.ts +2 -2
- package/src/page-action.ts +59 -0
- package/src/page-def.ts +9 -9
- package/src/page-flow.ts +4 -4
- package/src/popup.ts +3 -3
- package/src/repository.ts +35 -35
- package/src/task.ts +52 -0
- package/src/utils.ts +18 -4
package/src/aggregate.ts
CHANGED
|
@@ -1,104 +1,95 @@
|
|
|
1
|
-
import type { SchemaBase } from './dsl.js';
|
|
2
|
-
import type { TableSchema
|
|
3
|
-
|
|
4
|
-
// Aggregate declaration: groups multiple tables into one domain concept with
|
|
5
|
-
// a root table, member
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
throw new Error(
|
|
96
|
-
`aggregate '${options.name}': member '${role}' table '${member.table.name}' has ${fks.length} foreign keys referencing root '${options.root.name}' — declare via explicitly`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
(member as { via: ForeignKey }).via = fks[0];
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return schema;
|
|
1
|
+
import type { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { TableSchema } from './db.js';
|
|
3
|
+
|
|
4
|
+
// Aggregate declaration: groups multiple tables into one domain concept with
|
|
5
|
+
// a root table, member tables, cross-member invariants and inter-aggregate
|
|
6
|
+
// reference rules. This turns "multi-table consistency" from a convention
|
|
7
|
+
// (hand-written in flows) into a constraint (lintable, codegen-able).
|
|
8
|
+
//
|
|
9
|
+
// Members are intentionally minimal:
|
|
10
|
+
// members: { items: [orderItem] } -> array = 1:N (normal table)
|
|
11
|
+
// members: { address: orderAddress } -> single = 1:1 (extension table;
|
|
12
|
+
// designed but not implemented yet)
|
|
13
|
+
|
|
14
|
+
/** Member table(s) keyed by role name. Array = 1:N; non-array = 1:1 extension. */
|
|
15
|
+
export type AggregateMember = TableSchema | TableSchema[];
|
|
16
|
+
|
|
17
|
+
/** A cross-member invariant, checked by generated repository code. */
|
|
18
|
+
export interface AggregateInvariant {
|
|
19
|
+
name: string;
|
|
20
|
+
/** Expression in the aggregate's field vocabulary (e.g. 'total == sum(items.price * items.qty)'). */
|
|
21
|
+
check: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DomainAggregate extends SchemaBase {
|
|
25
|
+
type: 'aggregate';
|
|
26
|
+
/** The aggregate root table. */
|
|
27
|
+
root: TableSchema;
|
|
28
|
+
/** Member tables keyed by role name (e.g. 'items', 'address'). */
|
|
29
|
+
members: Record<string, AggregateMember>;
|
|
30
|
+
/** Cross-member invariants; optional. */
|
|
31
|
+
invariants?: AggregateInvariant[];
|
|
32
|
+
/** Inter-aggregate references: only by root ID, keyed by referenced role. */
|
|
33
|
+
references?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function defineAggregate(options: {
|
|
37
|
+
root: TableSchema;
|
|
38
|
+
members?: Record<string, AggregateMember>;
|
|
39
|
+
invariants?: AggregateInvariant[];
|
|
40
|
+
references?: Record<string, string>;
|
|
41
|
+
description?: string;
|
|
42
|
+
}): DomainAggregate {
|
|
43
|
+
const schema: DomainAggregate = {
|
|
44
|
+
type: 'aggregate',
|
|
45
|
+
name: options.root.name,
|
|
46
|
+
description: options.description,
|
|
47
|
+
root: options.root,
|
|
48
|
+
members: options.members ?? {},
|
|
49
|
+
invariants: options.invariants,
|
|
50
|
+
references: options.references,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Root must have a primary key (aggregate identity).
|
|
54
|
+
if (options.root.primaryKey === undefined) {
|
|
55
|
+
throw new Error(`aggregate '${schema.name}': root table '${options.root.name}' must have a primary key`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rootPkRefs = Array.isArray(options.root.primaryKey)
|
|
59
|
+
? options.root.primaryKey
|
|
60
|
+
: [options.root.primaryKey];
|
|
61
|
+
|
|
62
|
+
// Each member must attach to the root. Array members are normal 1:N tables
|
|
63
|
+
// and must have exactly one FK referencing the root. Non-array (1:1 extension)
|
|
64
|
+
// members are designed but not implemented yet.
|
|
65
|
+
for (const [role, member] of Object.entries(schema.members)) {
|
|
66
|
+
if (Array.isArray(member)) {
|
|
67
|
+
if (member.length !== 1) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`aggregate '${schema.name}': member '${role}' array must contain exactly one table schema`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const table = member[0];
|
|
73
|
+
const fks = Object.values(table.foreignKeys ?? {}).filter((fk) => {
|
|
74
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
75
|
+
return refs.length === rootPkRefs.length && refs.every((r) => rootPkRefs.includes(r));
|
|
76
|
+
});
|
|
77
|
+
if (fks.length === 0) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`aggregate '${schema.name}': member '${role}' table '${table.name}' has no foreign key referencing root '${options.root.name}' — declare one in the table's foreignKeys`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (fks.length > 1) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`aggregate '${schema.name}': member '${role}' table '${table.name}' has ${fks.length} foreign keys referencing root '${options.root.name}' — reduce to one FK for minimal aggregate declarations`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`aggregate '${schema.name}': member '${role}' is a non-array table — 1:1 extension tables are designed but not implemented yet; use an array for 1:N members`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return schema;
|
|
104
95
|
}
|
package/src/component.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { SchemaBase } from './dsl.js';
|
|
2
2
|
import type { RefSchema } from './ref.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { PageActionSchema } from './page-action.js';
|
|
4
4
|
import type { EventDataSchema } from './event.js';
|
|
5
5
|
|
|
6
6
|
/** A component event trigger declaration. */
|
|
7
7
|
export interface TriggerSchema extends SchemaBase {
|
|
8
8
|
/** Data the event carries (e.g. e.detail). */
|
|
9
9
|
eventData?: EventDataSchema;
|
|
10
|
-
/**
|
|
11
|
-
actions?:
|
|
10
|
+
/** Page actions that fire when the event occurs. */
|
|
11
|
+
actions?: PageActionSchema[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** A UI component declaration — a virtual schema that describes props and
|
package/src/curd.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SchemaBase, Field } from './dsl.js';
|
|
2
2
|
import { TableSchema } from './db.js';
|
|
3
3
|
import { FrontAppSchema } from './project.js';
|
|
4
|
-
import {
|
|
4
|
+
import { PageActionSchema } from './page-action.js';
|
|
5
5
|
import type { FilterSchema } from './filter.js';
|
|
6
6
|
import { toKebabCase } from '@pylonts/core';
|
|
7
7
|
|
|
@@ -51,7 +51,7 @@ export interface CurdSchema extends SchemaBase {
|
|
|
51
51
|
/** Sidebar menu section (group) this CRUD page belongs to. */
|
|
52
52
|
section: string;
|
|
53
53
|
/** Extra user actions on this page (beyond the standard CRUD). */
|
|
54
|
-
actions?:
|
|
54
|
+
actions?: PageActionSchema[];
|
|
55
55
|
/** Add/update/detail action pages. */
|
|
56
56
|
actionPages?: {
|
|
57
57
|
add?: ActionPage;
|
package/src/dto.ts
CHANGED
|
@@ -200,13 +200,55 @@ export function resolveDtoRefChain(f: DtoField): DtoField {
|
|
|
200
200
|
return cur;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
/** TS type of a DtoField in generated code
|
|
204
|
-
*
|
|
203
|
+
/** TS type of a DtoField in generated code.
|
|
204
|
+
* A field shared by reference (its schema is the owning DTO — utils args
|
|
205
|
+
* like `args: { items: OrderSubmitRequest.fields.items }`) renders as an
|
|
206
|
+
* indexed access on the DTO's generated type (the DTO owns the structure).
|
|
207
|
+
* Array elements render by name (`ItemDto[]` — named DTO) or by recursion
|
|
208
|
+
* (`Array<string>` — scalar). Plain wire objects (objectField) render
|
|
209
|
+
* their property shape (`{ key: type }`); DtoField-class containers are
|
|
210
|
+
* rejected at build time (DTOs must not nest inline structures).
|
|
211
|
+
* Enum → its JS name, date/datetime → string. */
|
|
205
212
|
export function dtoFieldJsType(df: DtoField): string {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
const owner = df.schema as { type?: string; name?: string } | undefined;
|
|
214
|
+
if (owner?.type === 'dto' && owner.name !== undefined && owner.name !== '' && df.name !== '') {
|
|
215
|
+
return `${owner.name}['${df.name}']`;
|
|
216
|
+
}
|
|
217
|
+
return dtoFieldJsTypeInner(df.field);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Type of a raw field object — unwraps DtoField-class wrappers
|
|
221
|
+
* (dtoField(dtoArrayField(...)) stores the def inside the instance's
|
|
222
|
+
* .field) and recurses: named-DTO elements (Name[]), scalar elements
|
|
223
|
+
* (Array<T>), plain wire objects ({ key: type }), enums, and scalars. */
|
|
224
|
+
function dtoFieldJsTypeInner(field: Field | DtoArrayFieldDef | DtoObjectFieldDef): string {
|
|
225
|
+
const f = (field as { field?: unknown }).field ?? field;
|
|
226
|
+
const inner = f as {
|
|
227
|
+
type?: string;
|
|
228
|
+
jsType?: string;
|
|
229
|
+
enum?: { jsName: string };
|
|
230
|
+
items?: DtoField | DtoMessage;
|
|
231
|
+
properties?: Record<string, DtoField | Field>;
|
|
232
|
+
};
|
|
233
|
+
if (inner.type === 'enum') return inner.enum!.jsName;
|
|
234
|
+
if (inner.type === 'date' || inner.type === 'datetime') return 'string';
|
|
235
|
+
if (inner.type === 'array') {
|
|
236
|
+
const items = inner.items!;
|
|
237
|
+
return isDtoMessage(items) ? `${items.name}[]` : `Array<${dtoFieldJsType(items)}>`;
|
|
238
|
+
}
|
|
239
|
+
if (inner.type === 'object') {
|
|
240
|
+
// Plain objectField properties are bare Fields; DtoObjectFieldDef
|
|
241
|
+
// properties are DtoFields. Recurse through both.
|
|
242
|
+
const props = Object.entries(inner.properties ?? {})
|
|
243
|
+
.map(([k, v]) => {
|
|
244
|
+
const optional = isDtoField(v) ? v.isOptional() : (v as Field).optional ?? false;
|
|
245
|
+
const type = isDtoField(v) ? dtoFieldJsType(v) : dtoFieldJsTypeInner(v as Field);
|
|
246
|
+
return `${k}${optional ? '?' : ''}: ${type}`;
|
|
247
|
+
})
|
|
248
|
+
.join('; ');
|
|
249
|
+
return `{ ${props} }`;
|
|
250
|
+
}
|
|
251
|
+
return inner.jsType ?? '';
|
|
210
252
|
}
|
|
211
253
|
|
|
212
254
|
/** Enum JS names referenced by a DtoField, recursing into inline array/object
|
|
@@ -225,8 +267,10 @@ export function dtoCollectEnumRefs(df: DtoField, out: string[] = []): string[] {
|
|
|
225
267
|
}
|
|
226
268
|
|
|
227
269
|
export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
|
|
228
|
-
// Items stay as-is:
|
|
229
|
-
//
|
|
270
|
+
// Items stay as-is: a DtoMessage is referenced by name (the driver renders
|
|
271
|
+
// Type.Array(<DtoName>)); a scalar DtoField element renders its primitive
|
|
272
|
+
// type. Inline container elements are rejected by buildMessage/defineUtils
|
|
273
|
+
// (DTOs must not nest inline structures — every object needs a name).
|
|
230
274
|
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
|
|
231
275
|
}
|
|
232
276
|
|
|
@@ -234,7 +278,42 @@ export function dtoObjectField(def: { properties: Record<string, DtoField> } & O
|
|
|
234
278
|
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
|
|
235
279
|
}
|
|
236
280
|
|
|
281
|
+
/** DTOs must not nest DtoField-class containers inline: dtoObjectField /
|
|
282
|
+
* dtoArrayField instances (and dtoField(dtoObjectField(...))-style wraps)
|
|
283
|
+
* have no reusable name — extract a named DTO and reference it as an array
|
|
284
|
+
* element (dtoArrayField({ items: namedDto })), and array items must be a
|
|
285
|
+
* named DTO or a scalar field. Plain Field containers (objectField /
|
|
286
|
+
* arrayField — wire-format nesting) stay legal and render inline.
|
|
287
|
+
* DtoField-class wrappers (dtoField(dtoArrayField(...))) carry the def
|
|
288
|
+
* inside the instance's .field, so both layers are unwrapped. */
|
|
289
|
+
export function assertNoInlineContainers(dtoName: string, fields: Record<string, DtoField>): void {
|
|
290
|
+
for (const [key, df] of Object.entries(fields)) {
|
|
291
|
+
const f = (df.field as { field?: unknown }).field ?? df.field;
|
|
292
|
+
const field = f as { type?: string; items?: DtoField | DtoMessage };
|
|
293
|
+
// Only DtoField-class containers are banned (they need a name). Plain
|
|
294
|
+
// Field objects (objectField — wire-format nesting) are legal and render
|
|
295
|
+
// inline: dtoField(objectField({...})) stays allowed.
|
|
296
|
+
const isDtoClassContainer =
|
|
297
|
+
isDtoField(df.field) || typeof (df as { properties?: unknown }).properties === 'function';
|
|
298
|
+
if (isDtoClassContainer && field.type === 'object') {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`[dto] "${dtoName}" field "${key}": inline object is not allowed — dtoObjectField containers must be named: extract a named DTO and reference it (dtoArrayField({ items: itemDto })) or use a plain objectField for wire-format nesting`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (field.type === 'array' && isDtoField(field.items)) {
|
|
304
|
+
const items = (field.items.field as { field?: unknown }).field ?? field.items.field;
|
|
305
|
+
const item = items as { type?: string };
|
|
306
|
+
if (item.type === 'object' || item.type === 'array') {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`[dto] "${dtoName}" field "${key}": inline container elements are not allowed — array items must be a named DTO or a scalar field`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
237
315
|
function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
316
|
+
assertNoInlineContainers(name, fields);
|
|
238
317
|
const message = new DtoMessage(name, direction, fields, description);
|
|
239
318
|
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
240
319
|
// are created per DTO, never shared).
|
package/src/flow-script.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
guard,
|
|
22
22
|
ifNode,
|
|
23
23
|
isCall,
|
|
24
|
+
isConditionGroup,
|
|
24
25
|
isEnd,
|
|
25
26
|
isFlowNode,
|
|
26
27
|
isFlowSlot,
|
|
@@ -297,6 +298,10 @@ function addUsed(ctx: FlowCompile, slot: FlowSlot | undefined): void {
|
|
|
297
298
|
}
|
|
298
299
|
|
|
299
300
|
function addConditionUsed(ctx: FlowCompile, c: GuardCondition): void {
|
|
301
|
+
if (isConditionGroup(c)) {
|
|
302
|
+
for (const s of c.conds) addConditionUsed(ctx, s);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
300
305
|
if (!isCall(c)) {
|
|
301
306
|
addUsed(ctx, isFlowSlot(c.field) ? c.field : c.field.slot);
|
|
302
307
|
return;
|
|
@@ -310,6 +315,8 @@ function isInvokeStep(c: GuardCondition | InvokeStep): c is InvokeStep {
|
|
|
310
315
|
return (c as InvokeStep).kind === 'invoke';
|
|
311
316
|
}
|
|
312
317
|
|
|
318
|
+
/** Lower an IF condition: a top-level invoke is a predicate call, and
|
|
319
|
+
* composites convert invoke steps nested inside them recursively. */
|
|
313
320
|
function toCondition(c: GuardCondition | InvokeStep): GuardCondition {
|
|
314
321
|
if (isInvokeStep(c)) {
|
|
315
322
|
if (c.result !== undefined) {
|
|
@@ -317,6 +324,9 @@ function toCondition(c: GuardCondition | InvokeStep): GuardCondition {
|
|
|
317
324
|
}
|
|
318
325
|
return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] };
|
|
319
326
|
}
|
|
327
|
+
if (isConditionGroup(c)) {
|
|
328
|
+
return { kind: c.kind, conds: c.conds.map((sub) => toCondition(sub)) };
|
|
329
|
+
}
|
|
320
330
|
return c;
|
|
321
331
|
}
|
|
322
332
|
|
|
@@ -341,11 +351,35 @@ function methodThrows(m: FlowMethodRef): ExceptionSchema[] {
|
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
/** Readable condition text used as node/branch labels (and as the throw
|
|
344
|
-
* label when THROW carries no message).
|
|
354
|
+
* label when THROW carries no message). Composites render parenthesized
|
|
355
|
+
* sub-conditions: !(a), (a && b), (a || b). */
|
|
345
356
|
function renderCondition(c: GuardCondition): string {
|
|
357
|
+
if (isConditionGroup(c)) {
|
|
358
|
+
const inner = c.conds.map(renderCondition).join(c.kind === 'and' ? ' && ' : c.kind === 'or' ? ' || ' : '');
|
|
359
|
+
return c.kind === 'not' ? `!(${inner})` : `(${inner})`;
|
|
360
|
+
}
|
|
346
361
|
if (!isCall(c)) {
|
|
347
362
|
if (isFlowSlot(c.field)) {
|
|
348
|
-
|
|
363
|
+
const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
|
|
364
|
+
if (nullOp) return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`;
|
|
365
|
+
// scalar slot comparison: total > 100
|
|
366
|
+
const ref = c.field.name;
|
|
367
|
+
switch (c.op) {
|
|
368
|
+
case 'lt':
|
|
369
|
+
return `${ref} < ${renderValue(c.value)}`;
|
|
370
|
+
case 'le':
|
|
371
|
+
return `${ref} <= ${renderValue(c.value)}`;
|
|
372
|
+
case 'gt':
|
|
373
|
+
return `${ref} > ${renderValue(c.value)}`;
|
|
374
|
+
case 'ge':
|
|
375
|
+
return `${ref} >= ${renderValue(c.value)}`;
|
|
376
|
+
case 'eq':
|
|
377
|
+
return `${ref} = ${renderValue(c.value)}`;
|
|
378
|
+
case 'ne':
|
|
379
|
+
return `${ref} ≠ ${renderValue(c.value)}`;
|
|
380
|
+
default:
|
|
381
|
+
throw new Error(`unsupported comparison op '${c.op}'`);
|
|
382
|
+
}
|
|
349
383
|
}
|
|
350
384
|
const field = c.field.field as { name: string };
|
|
351
385
|
const ref = `${c.field.slot.name}.${field.name}`;
|
|
@@ -546,15 +580,21 @@ function compileTry(step: TryStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inheri
|
|
|
546
580
|
const ordinal = ctx.tryTotal - ++ctx.tryCount + 1;
|
|
547
581
|
const suffix = ordinal === 1 ? '' : `${ordinal}`;
|
|
548
582
|
const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit);
|
|
583
|
+
// Catch handlers and finally may read what the body produced before the
|
|
584
|
+
// failure point (Java semantics: try { row = dao.get() } catch { use(row) }).
|
|
585
|
+
// The body's productions seed their entry availability alongside the
|
|
586
|
+
// enclosing flow's inherited slots.
|
|
587
|
+
const bodyProduced = flowProducedSlots(body);
|
|
588
|
+
const catchInherit = [...inherit, ...bodyProduced];
|
|
549
589
|
const catches = step.catches.map(([ex, steps]) => ({
|
|
550
590
|
exception: ex,
|
|
551
|
-
handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx,
|
|
591
|
+
handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, catchInherit),
|
|
552
592
|
}));
|
|
553
593
|
const t = tryNode(step.name ?? 'try', {
|
|
554
594
|
body,
|
|
555
595
|
catches,
|
|
556
596
|
finally: step.finally
|
|
557
|
-
? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx,
|
|
597
|
+
? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, catchInherit)
|
|
558
598
|
: undefined,
|
|
559
599
|
});
|
|
560
600
|
ctx.seen.add(t);
|
|
@@ -585,6 +625,22 @@ function compileSub(step: SubStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inheri
|
|
|
585
625
|
return n;
|
|
586
626
|
}
|
|
587
627
|
|
|
628
|
+
/** Slots a flow's own nodes produce (call results and writes) — the try
|
|
629
|
+
* body's productions become visible to its catch handlers and finally. */
|
|
630
|
+
function flowProducedSlots(f: FlowSchema): FlowSlot[] {
|
|
631
|
+
const out = new Set<FlowSlot>();
|
|
632
|
+
for (const n of f.nodes) {
|
|
633
|
+
if (isEnd(n)) continue;
|
|
634
|
+
if (isGuard(n) || isFlowNode(n)) {
|
|
635
|
+
for (const m of n.methods ?? []) {
|
|
636
|
+
if (isCall(m) && m.result !== undefined) out.add(m.result);
|
|
637
|
+
}
|
|
638
|
+
if (isFlowNode(n)) for (const w of n.writes ?? []) out.add(w);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return [...out];
|
|
642
|
+
}
|
|
643
|
+
|
|
588
644
|
/** Compile one flow (top-level or sub-flow): its own slots registry (args
|
|
589
645
|
* plus the named slots actually used inside), its own exception ends, and
|
|
590
646
|
* its own return end (linked through DANGLE). */
|
|
@@ -628,8 +684,11 @@ function buildFlow(
|
|
|
628
684
|
const nodes = [...ctx.seen];
|
|
629
685
|
const registry = defineSlots(buildSlots(ctx));
|
|
630
686
|
// Entry inheritance only for slots the flow actually consumes; unused
|
|
631
|
-
// productions of the enclosing flow are not this flow's concern.
|
|
632
|
-
|
|
687
|
+
// productions of the enclosing flow are not this flow's concern. Name-based
|
|
688
|
+
// matching: inherited instances may come from another flow's registry (the
|
|
689
|
+
// try body's re-bound productions), so identity comparison would drop them.
|
|
690
|
+
const usedNames = new Set([...ctx.usedSlots].map((s) => s.name));
|
|
691
|
+
const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => usedNames.has(s.name)));
|
|
633
692
|
return defineFlow(name, {
|
|
634
693
|
start,
|
|
635
694
|
description,
|
|
@@ -671,6 +730,9 @@ function rewriteSlots(nodes: FlowNodeOrEnd[], edges: FlowEdge[], slots: FlowSlot
|
|
|
671
730
|
return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined };
|
|
672
731
|
};
|
|
673
732
|
const cond = (c: GuardCondition): GuardCondition => {
|
|
733
|
+
if (isConditionGroup(c)) {
|
|
734
|
+
return { kind: c.kind, conds: c.conds.map(cond) };
|
|
735
|
+
}
|
|
674
736
|
if (!isCall(c)) {
|
|
675
737
|
if (isFlowSlot(c.field)) {
|
|
676
738
|
return { kind: 'comparison', op: c.op, field: map(c.field), value: c.value };
|