@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/README.md
CHANGED
|
@@ -7,21 +7,24 @@ Schema 定义与产物生成分离的 DSL 系统:先写 DSL 元数据(表 /
|
|
|
7
7
|
从概要设计到详细设计的自顶向下链路:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
11
|
-
→
|
|
12
|
-
→
|
|
13
|
-
→
|
|
14
|
-
→
|
|
10
|
+
concepts(概念:名词 + 名词解释——先于一切存在的名字层)
|
|
11
|
+
→ project(地图:应用与 API 拓扑)
|
|
12
|
+
→ dictionary(词典:词根与字段命名标准,概念派生)
|
|
13
|
+
→ prototype(概要设计:页面与字段)
|
|
14
|
+
→ table / dto(详细设计)
|
|
15
|
+
→ driver 生成产物(SQL / 枚举 / TypeBox)
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
## 文档
|
|
18
19
|
|
|
20
|
+
- [concepts.md](./docs/concepts.md) — 概念层(名词 + 名词解释,蓝图的名字层;设计草案)
|
|
19
21
|
- [project.md](./docs/project.md) — 项目拓扑(地图)
|
|
20
22
|
- [dictionary.md](./docs/dictionary.md) — 短语词典(命名标准)
|
|
21
23
|
- [prototype.md](./docs/prototype.md) — 页面原型(概要设计)
|
|
22
24
|
- [table.md](./docs/table.md) — 定义表(详细设计)
|
|
23
25
|
- [dto.md](./docs/dto.md) — 定义 DTO
|
|
24
26
|
- [curd.md](./docs/curd.md) — 管理端 CRUD 页面标准(CurdSchema)
|
|
27
|
+
- [task.md](./docs/task.md) — 系统任务(TaskSchema:scheduled 定时 / async 异步,action 闭包成员)
|
|
25
28
|
- [utils.md](./docs/utils.md) — 工具模块与领域规则(UtilsSchema:防御 guard / 判断 / 计算)
|
|
26
29
|
- [aggregate.md](./docs/aggregate.md) — 聚合/仓储 DSL 扩展规划(声明层已落地:DomainAggregate/RepositorySchema + 存储校验,生成器待选型)
|
|
27
30
|
- [domain-event.md](./docs/domain-event.md) — 领域事件 DSL 扩展规划(声明层已落地:DomainEventSchema + event_schema 存储校验,发布/订阅待实现)
|
package/dist/action.d.ts
CHANGED
|
@@ -1,39 +1,82 @@
|
|
|
1
1
|
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
|
|
3
|
-
import type { RefSchema } from './ref.js';
|
|
4
2
|
import type { ControllerMethodSchema } from './controller.js';
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import type { ThirdServiceMethodSchema, ThirdCallbackSchema } from './third-service.js';
|
|
4
|
+
import type { TaskSchema } from './task.js';
|
|
5
|
+
import type { TableSchema } from './db.js';
|
|
6
|
+
import type { PageSchema } from './page.js';
|
|
7
|
+
/** A beat of a journey: type + properties + input data. */
|
|
7
8
|
export interface ActionSchema extends SchemaBase {
|
|
8
|
-
type:
|
|
9
|
+
type: 'page' | 'controller' | 'third' | 'db' | 'task';
|
|
10
|
+
/** Input data of this beat (blueprint: names first, refined to refs later). */
|
|
11
|
+
data?: Record<string, unknown>;
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
/** Visit a page — `data` present means fill/submit a form, absent means pure view. */
|
|
14
|
+
export interface PageAction extends ActionSchema {
|
|
15
|
+
type: 'page';
|
|
16
|
+
/** Target page (PageFlow node). */
|
|
17
|
+
page: PageSchema;
|
|
18
|
+
/** Page url/path (e.g. '/bd/apply'). */
|
|
19
|
+
url: string;
|
|
20
|
+
}
|
|
21
|
+
/** Call a backend controller method. */
|
|
22
|
+
export interface ControllerAction extends ActionSchema {
|
|
23
|
+
type: 'controller';
|
|
24
|
+
/** The controller method invoked (shared instance). */
|
|
25
|
+
method: ControllerMethodSchema;
|
|
26
|
+
}
|
|
27
|
+
/** Invoke a third-party service method or receive its callback. */
|
|
28
|
+
export interface ThirdAction extends ActionSchema {
|
|
29
|
+
type: 'third';
|
|
30
|
+
/** The third-party method invoked (outbound). */
|
|
31
|
+
method?: ThirdServiceMethodSchema;
|
|
32
|
+
/** The third-party callback received (inbound) — mutually exclusive with method. */
|
|
33
|
+
callback?: ThirdCallbackSchema;
|
|
34
|
+
/** Wait for the async callback (inbound) before the journey continues. */
|
|
35
|
+
async?: boolean;
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
/** Write to a database table. */
|
|
38
|
+
export interface DbAction extends ActionSchema {
|
|
39
|
+
type: 'db';
|
|
40
|
+
/** The table written (shared instance). */
|
|
41
|
+
table: TableSchema;
|
|
42
|
+
/** Write operation: insert | update | delete. */
|
|
43
|
+
op: 'insert' | 'update' | 'delete';
|
|
38
44
|
}
|
|
39
|
-
|
|
45
|
+
/** Trigger a system task (scheduled or async). */
|
|
46
|
+
export interface TaskAction extends ActionSchema {
|
|
47
|
+
type: 'task';
|
|
48
|
+
/** The task triggered (shared instance). */
|
|
49
|
+
task: TaskSchema;
|
|
50
|
+
}
|
|
51
|
+
/** Builder for a journey action. */
|
|
52
|
+
export declare const action: {
|
|
53
|
+
page(options: {
|
|
54
|
+
page: PageSchema;
|
|
55
|
+
url: string;
|
|
56
|
+
data?: Record<string, unknown>;
|
|
57
|
+
description?: string;
|
|
58
|
+
}): PageAction;
|
|
59
|
+
controller(options: {
|
|
60
|
+
method: ControllerMethodSchema;
|
|
61
|
+
data?: Record<string, unknown>;
|
|
62
|
+
description?: string;
|
|
63
|
+
}): ControllerAction;
|
|
64
|
+
third(options: {
|
|
65
|
+
method?: ThirdServiceMethodSchema;
|
|
66
|
+
callback?: ThirdCallbackSchema;
|
|
67
|
+
data?: Record<string, unknown>;
|
|
68
|
+
async?: boolean;
|
|
69
|
+
description?: string;
|
|
70
|
+
}): ThirdAction;
|
|
71
|
+
db(options: {
|
|
72
|
+
table: TableSchema;
|
|
73
|
+
op: 'insert' | 'update' | 'delete';
|
|
74
|
+
data?: Record<string, unknown>;
|
|
75
|
+
description?: string;
|
|
76
|
+
}): DbAction;
|
|
77
|
+
task(options: {
|
|
78
|
+
task: TaskSchema;
|
|
79
|
+
data?: Record<string, unknown>;
|
|
80
|
+
description?: string;
|
|
81
|
+
}): TaskAction;
|
|
82
|
+
};
|
package/dist/action.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
1
|
+
/** Builder for a journey action. */
|
|
2
|
+
export const action = {
|
|
3
|
+
page(options) {
|
|
4
|
+
return { name: options.page.name, type: 'page', page: options.page, url: options.url, data: options.data, description: options.description };
|
|
5
|
+
},
|
|
6
|
+
controller(options) {
|
|
7
|
+
return { name: options.method.name, type: 'controller', method: options.method, data: options.data, description: options.description };
|
|
8
|
+
},
|
|
9
|
+
third(options) {
|
|
10
|
+
const name = options.method?.name ?? options.callback?.name ?? 'third';
|
|
11
|
+
return { name, type: 'third', method: options.method, callback: options.callback, data: options.data, async: options.async, description: options.description };
|
|
12
|
+
},
|
|
13
|
+
db(options) {
|
|
14
|
+
return { name: options.table.name, type: 'db', table: options.table, op: options.op, data: options.data, description: options.description };
|
|
15
|
+
},
|
|
16
|
+
task(options) {
|
|
17
|
+
return { name: options.task.name, type: 'task', task: options.task, data: options.data, description: options.description };
|
|
18
|
+
},
|
|
19
|
+
};
|
package/dist/aggregate.d.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import type { SchemaBase } from './dsl.js';
|
|
2
|
-
import type { TableSchema
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
5
|
-
/** The member table (shared instance from schema/*.table.ts). */
|
|
6
|
-
table: TableSchema;
|
|
7
|
-
/** The foreign key on the member table pointing to the root table.
|
|
8
|
-
* Must exist in table.foreignKeys and its references must be the root's PK
|
|
9
|
-
* columns. Defaults to the (unique) FK referencing the root table. */
|
|
10
|
-
via?: ForeignKey;
|
|
11
|
-
/** 1:1 member (unique constraint on via.columns) vs 1:N (default). */
|
|
12
|
-
one?: boolean;
|
|
13
|
-
}
|
|
2
|
+
import type { TableSchema } from './db.js';
|
|
3
|
+
/** Member table(s) keyed by role name. Array = 1:N; non-array = 1:1 extension. */
|
|
4
|
+
export type AggregateMember = TableSchema | TableSchema[];
|
|
14
5
|
/** A cross-member invariant, checked by generated repository code. */
|
|
15
6
|
export interface AggregateInvariant {
|
|
16
7
|
name: string;
|
|
@@ -29,7 +20,6 @@ export interface DomainAggregate extends SchemaBase {
|
|
|
29
20
|
references?: Record<string, string>;
|
|
30
21
|
}
|
|
31
22
|
export declare function defineAggregate(options: {
|
|
32
|
-
name: string;
|
|
33
23
|
root: TableSchema;
|
|
34
24
|
members?: Record<string, AggregateMember>;
|
|
35
25
|
invariants?: AggregateInvariant[];
|
package/dist/aggregate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function defineAggregate(options) {
|
|
2
2
|
const schema = {
|
|
3
3
|
type: 'aggregate',
|
|
4
|
-
name: options.name,
|
|
4
|
+
name: options.root.name,
|
|
5
5
|
description: options.description,
|
|
6
6
|
root: options.root,
|
|
7
7
|
members: options.members ?? {},
|
|
@@ -10,36 +10,33 @@ export function defineAggregate(options) {
|
|
|
10
10
|
};
|
|
11
11
|
// Root must have a primary key (aggregate identity).
|
|
12
12
|
if (options.root.primaryKey === undefined) {
|
|
13
|
-
throw new Error(`aggregate '${
|
|
13
|
+
throw new Error(`aggregate '${schema.name}': root table '${options.root.name}' must have a primary key`);
|
|
14
14
|
}
|
|
15
|
-
// Each member must attach to the root via an existing FK referencing the root.
|
|
16
15
|
const rootPkRefs = Array.isArray(options.root.primaryKey)
|
|
17
16
|
? options.root.primaryKey
|
|
18
17
|
: [options.root.primaryKey];
|
|
18
|
+
// Each member must attach to the root. Array members are normal 1:N tables
|
|
19
|
+
// and must have exactly one FK referencing the root. Non-array (1:1 extension)
|
|
20
|
+
// members are designed but not implemented yet.
|
|
19
21
|
for (const [role, member] of Object.entries(schema.members)) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
if (fks.length === 0) {
|
|
25
|
-
throw new Error(`aggregate '${options.name}': member '${role}' table '${member.table.name}' has no foreign key referencing root '${options.root.name}' — declare one in the table's foreignKeys`);
|
|
26
|
-
}
|
|
27
|
-
if (member.via !== undefined) {
|
|
28
|
-
const viaKeys = Object.values(member.table.foreignKeys ?? {});
|
|
29
|
-
if (!viaKeys.includes(member.via)) {
|
|
30
|
-
throw new Error(`aggregate '${options.name}': member '${role}' via must be one of table '${member.table.name}' foreignKeys — got a non-FK object`);
|
|
22
|
+
if (Array.isArray(member)) {
|
|
23
|
+
if (member.length !== 1) {
|
|
24
|
+
throw new Error(`aggregate '${schema.name}': member '${role}' array must contain exactly one table schema`);
|
|
31
25
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
const table = member[0];
|
|
27
|
+
const fks = Object.values(table.foreignKeys ?? {}).filter((fk) => {
|
|
28
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
29
|
+
return refs.length === rootPkRefs.length && refs.every((r) => rootPkRefs.includes(r));
|
|
30
|
+
});
|
|
31
|
+
if (fks.length === 0) {
|
|
32
|
+
throw new Error(`aggregate '${schema.name}': member '${role}' table '${table.name}' has no foreign key referencing root '${options.root.name}' — declare one in the table's foreignKeys`);
|
|
35
33
|
}
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
// Default: the (single) FK referencing the root. More than one → must declare via.
|
|
39
34
|
if (fks.length > 1) {
|
|
40
|
-
throw new Error(`aggregate '${
|
|
35
|
+
throw new Error(`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`);
|
|
41
36
|
}
|
|
42
|
-
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(`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`);
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
return schema;
|
package/dist/component.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
|
/** A component event trigger declaration. */
|
|
6
6
|
export interface TriggerSchema extends SchemaBase {
|
|
7
7
|
/** Data the event carries (e.g. e.detail). */
|
|
8
8
|
eventData?: EventDataSchema;
|
|
9
|
-
/**
|
|
10
|
-
actions?:
|
|
9
|
+
/** Page actions that fire when the event occurs. */
|
|
10
|
+
actions?: PageActionSchema[];
|
|
11
11
|
}
|
|
12
12
|
/** A UI component declaration — a virtual schema that describes props and
|
|
13
13
|
* event triggers, not a real renderable component.
|
package/dist/curd.d.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
|
/** Mode of an action page: modal dialog or standalone route. */
|
|
7
7
|
export type ActionPageMode = 'modal' | 'route';
|
|
@@ -43,7 +43,7 @@ export interface CurdSchema extends SchemaBase {
|
|
|
43
43
|
/** Sidebar menu section (group) this CRUD page belongs to. */
|
|
44
44
|
section: string;
|
|
45
45
|
/** Extra user actions on this page (beyond the standard CRUD). */
|
|
46
|
-
actions?:
|
|
46
|
+
actions?: PageActionSchema[];
|
|
47
47
|
/** Add/update/detail action pages. */
|
|
48
48
|
actionPages?: {
|
|
49
49
|
add?: ActionPage;
|
package/dist/dto.d.ts
CHANGED
|
@@ -106,8 +106,15 @@ export declare function isDtoField(v: unknown): v is DtoField;
|
|
|
106
106
|
/** Resolve a ref chain to its terminal DtoField (the one without .ref).
|
|
107
107
|
* Cycles are a DSL definition error — fail loudly at render time. */
|
|
108
108
|
export declare function resolveDtoRefChain(f: DtoField): DtoField;
|
|
109
|
-
/** TS type of a DtoField in generated code
|
|
110
|
-
*
|
|
109
|
+
/** TS type of a DtoField in generated code.
|
|
110
|
+
* A field shared by reference (its schema is the owning DTO — utils args
|
|
111
|
+
* like `args: { items: OrderSubmitRequest.fields.items }`) renders as an
|
|
112
|
+
* indexed access on the DTO's generated type (the DTO owns the structure).
|
|
113
|
+
* Array elements render by name (`ItemDto[]` — named DTO) or by recursion
|
|
114
|
+
* (`Array<string>` — scalar). Plain wire objects (objectField) render
|
|
115
|
+
* their property shape (`{ key: type }`); DtoField-class containers are
|
|
116
|
+
* rejected at build time (DTOs must not nest inline structures).
|
|
117
|
+
* Enum → its JS name, date/datetime → string. */
|
|
111
118
|
export declare function dtoFieldJsType(df: DtoField): string;
|
|
112
119
|
/** Enum JS names referenced by a DtoField, recursing into inline array/object
|
|
113
120
|
* wrappers; DtoMessage item references stop the walk. First-occurrence order. */
|
|
@@ -118,6 +125,15 @@ export declare function dtoArrayField(def: {
|
|
|
118
125
|
export declare function dtoObjectField(def: {
|
|
119
126
|
properties: Record<string, DtoField>;
|
|
120
127
|
} & Omit<BaseField, 'name'>): DtoObjectField;
|
|
128
|
+
/** DTOs must not nest DtoField-class containers inline: dtoObjectField /
|
|
129
|
+
* dtoArrayField instances (and dtoField(dtoObjectField(...))-style wraps)
|
|
130
|
+
* have no reusable name — extract a named DTO and reference it as an array
|
|
131
|
+
* element (dtoArrayField({ items: namedDto })), and array items must be a
|
|
132
|
+
* named DTO or a scalar field. Plain Field containers (objectField /
|
|
133
|
+
* arrayField — wire-format nesting) stay legal and render inline.
|
|
134
|
+
* DtoField-class wrappers (dtoField(dtoArrayField(...))) carry the def
|
|
135
|
+
* inside the instance's .field, so both layers are unwrapped. */
|
|
136
|
+
export declare function assertNoInlineContainers(dtoName: string, fields: Record<string, DtoField>): void;
|
|
121
137
|
export declare function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
122
138
|
export declare function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
123
139
|
export declare function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
package/dist/dto.js
CHANGED
|
@@ -133,15 +133,50 @@ export function resolveDtoRefChain(f) {
|
|
|
133
133
|
}
|
|
134
134
|
return cur;
|
|
135
135
|
}
|
|
136
|
-
/** TS type of a DtoField in generated code
|
|
137
|
-
*
|
|
136
|
+
/** TS type of a DtoField in generated code.
|
|
137
|
+
* A field shared by reference (its schema is the owning DTO — utils args
|
|
138
|
+
* like `args: { items: OrderSubmitRequest.fields.items }`) renders as an
|
|
139
|
+
* indexed access on the DTO's generated type (the DTO owns the structure).
|
|
140
|
+
* Array elements render by name (`ItemDto[]` — named DTO) or by recursion
|
|
141
|
+
* (`Array<string>` — scalar). Plain wire objects (objectField) render
|
|
142
|
+
* their property shape (`{ key: type }`); DtoField-class containers are
|
|
143
|
+
* rejected at build time (DTOs must not nest inline structures).
|
|
144
|
+
* Enum → its JS name, date/datetime → string. */
|
|
138
145
|
export function dtoFieldJsType(df) {
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
return
|
|
142
|
-
|
|
146
|
+
const owner = df.schema;
|
|
147
|
+
if (owner?.type === 'dto' && owner.name !== undefined && owner.name !== '' && df.name !== '') {
|
|
148
|
+
return `${owner.name}['${df.name}']`;
|
|
149
|
+
}
|
|
150
|
+
return dtoFieldJsTypeInner(df.field);
|
|
151
|
+
}
|
|
152
|
+
/** Type of a raw field object — unwraps DtoField-class wrappers
|
|
153
|
+
* (dtoField(dtoArrayField(...)) stores the def inside the instance's
|
|
154
|
+
* .field) and recurses: named-DTO elements (Name[]), scalar elements
|
|
155
|
+
* (Array<T>), plain wire objects ({ key: type }), enums, and scalars. */
|
|
156
|
+
function dtoFieldJsTypeInner(field) {
|
|
157
|
+
const f = field.field ?? field;
|
|
158
|
+
const inner = f;
|
|
159
|
+
if (inner.type === 'enum')
|
|
160
|
+
return inner.enum.jsName;
|
|
161
|
+
if (inner.type === 'date' || inner.type === 'datetime')
|
|
143
162
|
return 'string';
|
|
144
|
-
|
|
163
|
+
if (inner.type === 'array') {
|
|
164
|
+
const items = inner.items;
|
|
165
|
+
return isDtoMessage(items) ? `${items.name}[]` : `Array<${dtoFieldJsType(items)}>`;
|
|
166
|
+
}
|
|
167
|
+
if (inner.type === 'object') {
|
|
168
|
+
// Plain objectField properties are bare Fields; DtoObjectFieldDef
|
|
169
|
+
// properties are DtoFields. Recurse through both.
|
|
170
|
+
const props = Object.entries(inner.properties ?? {})
|
|
171
|
+
.map(([k, v]) => {
|
|
172
|
+
const optional = isDtoField(v) ? v.isOptional() : v.optional ?? false;
|
|
173
|
+
const type = isDtoField(v) ? dtoFieldJsType(v) : dtoFieldJsTypeInner(v);
|
|
174
|
+
return `${k}${optional ? '?' : ''}: ${type}`;
|
|
175
|
+
})
|
|
176
|
+
.join('; ');
|
|
177
|
+
return `{ ${props} }`;
|
|
178
|
+
}
|
|
179
|
+
return inner.jsType ?? '';
|
|
145
180
|
}
|
|
146
181
|
/** Enum JS names referenced by a DtoField, recursing into inline array/object
|
|
147
182
|
* wrappers; DtoMessage item references stop the walk. First-occurrence order. */
|
|
@@ -162,14 +197,45 @@ export function dtoCollectEnumRefs(df, out = []) {
|
|
|
162
197
|
return out;
|
|
163
198
|
}
|
|
164
199
|
export function dtoArrayField(def) {
|
|
165
|
-
// Items stay as-is:
|
|
166
|
-
//
|
|
200
|
+
// Items stay as-is: a DtoMessage is referenced by name (the driver renders
|
|
201
|
+
// Type.Array(<DtoName>)); a scalar DtoField element renders its primitive
|
|
202
|
+
// type. Inline container elements are rejected by buildMessage/defineUtils
|
|
203
|
+
// (DTOs must not nest inline structures — every object needs a name).
|
|
167
204
|
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
|
|
168
205
|
}
|
|
169
206
|
export function dtoObjectField(def) {
|
|
170
207
|
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
|
|
171
208
|
}
|
|
209
|
+
/** DTOs must not nest DtoField-class containers inline: dtoObjectField /
|
|
210
|
+
* dtoArrayField instances (and dtoField(dtoObjectField(...))-style wraps)
|
|
211
|
+
* have no reusable name — extract a named DTO and reference it as an array
|
|
212
|
+
* element (dtoArrayField({ items: namedDto })), and array items must be a
|
|
213
|
+
* named DTO or a scalar field. Plain Field containers (objectField /
|
|
214
|
+
* arrayField — wire-format nesting) stay legal and render inline.
|
|
215
|
+
* DtoField-class wrappers (dtoField(dtoArrayField(...))) carry the def
|
|
216
|
+
* inside the instance's .field, so both layers are unwrapped. */
|
|
217
|
+
export function assertNoInlineContainers(dtoName, fields) {
|
|
218
|
+
for (const [key, df] of Object.entries(fields)) {
|
|
219
|
+
const f = df.field.field ?? df.field;
|
|
220
|
+
const field = f;
|
|
221
|
+
// Only DtoField-class containers are banned (they need a name). Plain
|
|
222
|
+
// Field objects (objectField — wire-format nesting) are legal and render
|
|
223
|
+
// inline: dtoField(objectField({...})) stays allowed.
|
|
224
|
+
const isDtoClassContainer = isDtoField(df.field) || typeof df.properties === 'function';
|
|
225
|
+
if (isDtoClassContainer && field.type === 'object') {
|
|
226
|
+
throw new Error(`[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`);
|
|
227
|
+
}
|
|
228
|
+
if (field.type === 'array' && isDtoField(field.items)) {
|
|
229
|
+
const items = field.items.field.field ?? field.items.field;
|
|
230
|
+
const item = items;
|
|
231
|
+
if (item.type === 'object' || item.type === 'array') {
|
|
232
|
+
throw new Error(`[dto] "${dtoName}" field "${key}": inline container elements are not allowed — array items must be a named DTO or a scalar field`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
172
237
|
function buildMessage(name, direction, fields, description) {
|
|
238
|
+
assertNoInlineContainers(name, fields);
|
|
173
239
|
const message = new DtoMessage(name, direction, fields, description);
|
|
174
240
|
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
175
241
|
// are created per DTO, never shared).
|
package/dist/flow-script.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The IR stays the single executable model (mermaid, must-analysis, throws
|
|
10
10
|
// coverage, service contracts all consume it); this layer is a lowering, not
|
|
11
11
|
// a parallel model.
|
|
12
|
-
import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isEnd, isFlowNode, isFlowSlot, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
|
|
12
|
+
import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isConditionGroup, isEnd, isFlowNode, isFlowSlot, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
|
|
13
13
|
/** Call a method as a statement or (in IF position) as a utils predicate. */
|
|
14
14
|
export function invoke(method, args, result) {
|
|
15
15
|
return { kind: 'invoke', method, args, result };
|
|
@@ -132,6 +132,11 @@ function addUsed(ctx, slot) {
|
|
|
132
132
|
ctx.usedNames.add(slot.name);
|
|
133
133
|
}
|
|
134
134
|
function addConditionUsed(ctx, c) {
|
|
135
|
+
if (isConditionGroup(c)) {
|
|
136
|
+
for (const s of c.conds)
|
|
137
|
+
addConditionUsed(ctx, s);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
135
140
|
if (!isCall(c)) {
|
|
136
141
|
addUsed(ctx, isFlowSlot(c.field) ? c.field : c.field.slot);
|
|
137
142
|
return;
|
|
@@ -144,6 +149,8 @@ function addConditionUsed(ctx, c) {
|
|
|
144
149
|
function isInvokeStep(c) {
|
|
145
150
|
return c.kind === 'invoke';
|
|
146
151
|
}
|
|
152
|
+
/** Lower an IF condition: a top-level invoke is a predicate call, and
|
|
153
|
+
* composites convert invoke steps nested inside them recursively. */
|
|
147
154
|
function toCondition(c) {
|
|
148
155
|
if (isInvokeStep(c)) {
|
|
149
156
|
if (c.result !== undefined) {
|
|
@@ -151,6 +158,9 @@ function toCondition(c) {
|
|
|
151
158
|
}
|
|
152
159
|
return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] };
|
|
153
160
|
}
|
|
161
|
+
if (isConditionGroup(c)) {
|
|
162
|
+
return { kind: c.kind, conds: c.conds.map((sub) => toCondition(sub)) };
|
|
163
|
+
}
|
|
154
164
|
return c;
|
|
155
165
|
}
|
|
156
166
|
/** Display form mirrors the mermaid driver: owner.name / schema.name. */
|
|
@@ -172,11 +182,36 @@ function methodThrows(m) {
|
|
|
172
182
|
return 'throws' in m && m.throws !== undefined ? m.throws : [];
|
|
173
183
|
}
|
|
174
184
|
/** Readable condition text used as node/branch labels (and as the throw
|
|
175
|
-
* label when THROW carries no message).
|
|
185
|
+
* label when THROW carries no message). Composites render parenthesized
|
|
186
|
+
* sub-conditions: !(a), (a && b), (a || b). */
|
|
176
187
|
function renderCondition(c) {
|
|
188
|
+
if (isConditionGroup(c)) {
|
|
189
|
+
const inner = c.conds.map(renderCondition).join(c.kind === 'and' ? ' && ' : c.kind === 'or' ? ' || ' : '');
|
|
190
|
+
return c.kind === 'not' ? `!(${inner})` : `(${inner})`;
|
|
191
|
+
}
|
|
177
192
|
if (!isCall(c)) {
|
|
178
193
|
if (isFlowSlot(c.field)) {
|
|
179
|
-
|
|
194
|
+
const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
|
|
195
|
+
if (nullOp)
|
|
196
|
+
return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`;
|
|
197
|
+
// scalar slot comparison: total > 100
|
|
198
|
+
const ref = c.field.name;
|
|
199
|
+
switch (c.op) {
|
|
200
|
+
case 'lt':
|
|
201
|
+
return `${ref} < ${renderValue(c.value)}`;
|
|
202
|
+
case 'le':
|
|
203
|
+
return `${ref} <= ${renderValue(c.value)}`;
|
|
204
|
+
case 'gt':
|
|
205
|
+
return `${ref} > ${renderValue(c.value)}`;
|
|
206
|
+
case 'ge':
|
|
207
|
+
return `${ref} >= ${renderValue(c.value)}`;
|
|
208
|
+
case 'eq':
|
|
209
|
+
return `${ref} = ${renderValue(c.value)}`;
|
|
210
|
+
case 'ne':
|
|
211
|
+
return `${ref} ≠ ${renderValue(c.value)}`;
|
|
212
|
+
default:
|
|
213
|
+
throw new Error(`unsupported comparison op '${c.op}'`);
|
|
214
|
+
}
|
|
180
215
|
}
|
|
181
216
|
const field = c.field.field;
|
|
182
217
|
const ref = `${c.field.slot.name}.${field.name}`;
|
|
@@ -363,15 +398,21 @@ function compileTry(step, ctx, cont, inherit) {
|
|
|
363
398
|
const ordinal = ctx.tryTotal - ++ctx.tryCount + 1;
|
|
364
399
|
const suffix = ordinal === 1 ? '' : `${ordinal}`;
|
|
365
400
|
const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit);
|
|
401
|
+
// Catch handlers and finally may read what the body produced before the
|
|
402
|
+
// failure point (Java semantics: try { row = dao.get() } catch { use(row) }).
|
|
403
|
+
// The body's productions seed their entry availability alongside the
|
|
404
|
+
// enclosing flow's inherited slots.
|
|
405
|
+
const bodyProduced = flowProducedSlots(body);
|
|
406
|
+
const catchInherit = [...inherit, ...bodyProduced];
|
|
366
407
|
const catches = step.catches.map(([ex, steps]) => ({
|
|
367
408
|
exception: ex,
|
|
368
|
-
handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx,
|
|
409
|
+
handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, catchInherit),
|
|
369
410
|
}));
|
|
370
411
|
const t = tryNode(step.name ?? 'try', {
|
|
371
412
|
body,
|
|
372
413
|
catches,
|
|
373
414
|
finally: step.finally
|
|
374
|
-
? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx,
|
|
415
|
+
? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, catchInherit)
|
|
375
416
|
: undefined,
|
|
376
417
|
});
|
|
377
418
|
ctx.seen.add(t);
|
|
@@ -401,6 +442,25 @@ function compileSub(step, ctx, cont, inherit) {
|
|
|
401
442
|
ctx.edges.push(edge(n, cont));
|
|
402
443
|
return n;
|
|
403
444
|
}
|
|
445
|
+
/** Slots a flow's own nodes produce (call results and writes) — the try
|
|
446
|
+
* body's productions become visible to its catch handlers and finally. */
|
|
447
|
+
function flowProducedSlots(f) {
|
|
448
|
+
const out = new Set();
|
|
449
|
+
for (const n of f.nodes) {
|
|
450
|
+
if (isEnd(n))
|
|
451
|
+
continue;
|
|
452
|
+
if (isGuard(n) || isFlowNode(n)) {
|
|
453
|
+
for (const m of n.methods ?? []) {
|
|
454
|
+
if (isCall(m) && m.result !== undefined)
|
|
455
|
+
out.add(m.result);
|
|
456
|
+
}
|
|
457
|
+
if (isFlowNode(n))
|
|
458
|
+
for (const w of n.writes ?? [])
|
|
459
|
+
out.add(w);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return [...out];
|
|
463
|
+
}
|
|
404
464
|
/** Compile one flow (top-level or sub-flow): its own slots registry (args
|
|
405
465
|
* plus the named slots actually used inside), its own exception ends, and
|
|
406
466
|
* its own return end (linked through DANGLE). */
|
|
@@ -432,8 +492,11 @@ function buildFlow(name, description, ctx, start) {
|
|
|
432
492
|
const nodes = [...ctx.seen];
|
|
433
493
|
const registry = defineSlots(buildSlots(ctx));
|
|
434
494
|
// Entry inheritance only for slots the flow actually consumes; unused
|
|
435
|
-
// productions of the enclosing flow are not this flow's concern.
|
|
436
|
-
|
|
495
|
+
// productions of the enclosing flow are not this flow's concern. Name-based
|
|
496
|
+
// matching: inherited instances may come from another flow's registry (the
|
|
497
|
+
// try body's re-bound productions), so identity comparison would drop them.
|
|
498
|
+
const usedNames = new Set([...ctx.usedSlots].map((s) => s.name));
|
|
499
|
+
const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => usedNames.has(s.name)));
|
|
437
500
|
return defineFlow(name, {
|
|
438
501
|
start,
|
|
439
502
|
description,
|
|
@@ -475,6 +538,9 @@ function rewriteSlots(nodes, edges, slots, entrySlots) {
|
|
|
475
538
|
return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined };
|
|
476
539
|
};
|
|
477
540
|
const cond = (c) => {
|
|
541
|
+
if (isConditionGroup(c)) {
|
|
542
|
+
return { kind: c.kind, conds: c.conds.map(cond) };
|
|
543
|
+
}
|
|
478
544
|
if (!isCall(c)) {
|
|
479
545
|
if (isFlowSlot(c.field)) {
|
|
480
546
|
return { kind: 'comparison', op: c.op, field: map(c.field), value: c.value };
|