@pylonts/dsl 1.0.0 → 1.0.2
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 +17 -85
- package/dist/dictionary.d.ts +8 -0
- package/dist/dictionary.js +7 -0
- package/dist/dsl.d.ts +19 -5
- package/dist/dsl.js +24 -4
- package/dist/enum-driver.d.ts +2 -2
- package/dist/enum-driver.js +5 -7
- package/dist/flow.d.ts +28 -0
- package/dist/flow.js +68 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mermaid-driver.d.ts +4 -0
- package/dist/mermaid-driver.js +72 -0
- package/dist/mysql-driver.d.ts +5 -1
- package/dist/mysql-driver.js +21 -4
- package/dist/page-flow.d.ts +21 -0
- package/dist/page-flow.js +25 -0
- package/dist/page.d.ts +26 -0
- package/dist/page.js +14 -0
- package/dist/pattern.d.ts +15 -0
- package/dist/pattern.js +13 -0
- package/dist/patterns/retry.d.ts +16 -0
- package/dist/patterns/retry.js +40 -0
- package/dist/project.d.ts +41 -0
- package/dist/project.js +11 -0
- package/dist/prototype.d.ts +18 -0
- package/dist/prototype.js +10 -0
- package/dist/typebox-driver.js +4 -8
- package/docs/dictionary.md +20 -0
- package/docs/driver.md +42 -0
- package/docs/dto.md +54 -0
- package/docs/enum.md +24 -0
- package/docs/mysql-connection.md +1 -0
- package/docs/project.md +24 -0
- package/docs/prototype.md +21 -0
- package/docs/table.md +90 -0
- package/package.json +3 -2
- package/src/dictionary.ts +15 -0
- package/src/dsl.ts +50 -8
- package/src/enum-driver.ts +7 -8
- package/src/flow.ts +104 -0
- package/src/index.ts +9 -0
- package/src/mermaid-driver.ts +81 -0
- package/src/mysql-driver.ts +26 -5
- package/src/page-flow.ts +52 -0
- package/src/page.ts +40 -0
- package/src/pattern.ts +27 -0
- package/src/patterns/retry.ts +55 -0
- package/src/project.ts +55 -0
- package/src/prototype.ts +30 -0
- package/src/typebox-driver.ts +4 -6
package/README.md
CHANGED
|
@@ -1,94 +1,26 @@
|
|
|
1
1
|
# @pylonts/dsl
|
|
2
2
|
|
|
3
|
-
Schema 定义与产物生成分离的 DSL 系统:先写 DSL 元数据(表 / DTO
|
|
3
|
+
Schema 定义与产物生成分离的 DSL 系统:先写 DSL 元数据(表 / DTO / 原型 / 项目 / 词典),再由 driver 翻译成目标产物(SQL、TS 枚举、TypeBox 源码)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 分层结构
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
dsl/
|
|
9
|
-
├── src/
|
|
10
|
-
│ ├── dsl.ts # 表 / 字段定义 + builder(stringField / intField / buildTable …)
|
|
11
|
-
│ ├── dto.ts # 接口层:buildInput / buildOutput / buildQuery / buildPk / from / include / op
|
|
12
|
-
│ ├── mysql-driver.ts # 表 → CREATE TABLE SQL
|
|
13
|
-
│ ├── enum-driver.ts # 枚举字段 → TS enum + LABEL 映射源码
|
|
14
|
-
│ └── typebox-driver.ts # DTO → TypeBox 源码(fastify v5 校验)
|
|
15
|
-
└── package.json
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## 核心概念:driver 模式
|
|
19
|
-
|
|
20
|
-
DSL 定义元数据,driver 翻译成目标语言产物。产物与定义解耦,同一份定义可生成不同目标:
|
|
21
|
-
|
|
22
|
-
| 定义 | Driver | 产物 | 消费方 |
|
|
23
|
-
|---|---|---|---|
|
|
24
|
-
| `TableSchema` | mysql-driver | `CREATE TABLE` | MySQL |
|
|
25
|
-
| `EnumField` | enum-driver | `export enum Xxx { … }` + `XXX_LABEL` | 业务代码 |
|
|
26
|
-
| `DtoMessage` | typebox-driver | `Type.Object({…})` + `Static` 推导 | fastify v5 参数校验 |
|
|
7
|
+
从概要设计到详细设计的自顶向下链路:
|
|
27
8
|
|
|
28
|
-
## 用法
|
|
29
|
-
|
|
30
|
-
### 定义表
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
import { bigintField, buildTable, decimalField, stringField } from './src/dsl';
|
|
34
|
-
|
|
35
|
-
const id = bigintField({ readOnly: true, label: '主键' });
|
|
36
|
-
const merId = bigintField({ label: '商户' });
|
|
37
|
-
|
|
38
|
-
export const order = buildTable('order', {
|
|
39
|
-
description: '订单',
|
|
40
|
-
generator: 'auto_increment',
|
|
41
|
-
fields: {
|
|
42
|
-
id,
|
|
43
|
-
order_no: stringField({ label: '订单号', maxLength: 32, optional: false }),
|
|
44
|
-
mer_id: merId,
|
|
45
|
-
amount: decimalField({ precision: 18, scale: 2, label: '金额' }),
|
|
46
|
-
},
|
|
47
|
-
primaryKey: id,
|
|
48
|
-
indexes: [{ fields: merId }],
|
|
49
|
-
});
|
|
50
9
|
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// 输入:新增订单
|
|
58
|
-
buildInput('OrderAddRequest', { ...from(order, [order.fields.mer_id, order.fields.amount]) });
|
|
59
|
-
|
|
60
|
-
// 输出:订单行
|
|
61
|
-
buildOutput('OrderRow', from(order, [order.fields.id, order.fields.order_no]));
|
|
62
|
-
|
|
63
|
-
// 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
|
|
64
|
-
buildQuery('OrderPageQuery', {
|
|
65
|
-
keyword: dtoField(stringField({ maxLength: 32 })).op('like'),
|
|
66
|
-
...from(order, [order.fields.mer_id]),
|
|
67
|
-
}).include({ from: '@pylonts/core', name: 'PageRequest' });
|
|
68
|
-
|
|
69
|
-
// 主键:按 id 取详情
|
|
70
|
-
buildPk('OrderDetailRequest', from(order, [order.fields.id]));
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 生成产物
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
import { buildCreateTableSql } from './src/mysql-driver';
|
|
77
|
-
import { renderEnum } from './src/enum-driver';
|
|
78
|
-
import { renderDtoMessage } from './src/typebox-driver';
|
|
79
|
-
|
|
80
|
-
buildCreateTableSql(order); // SQL
|
|
81
|
-
renderEnum(statusField); // TS enum 源码
|
|
82
|
-
renderDtoMessage(orderPageQuery, {
|
|
83
|
-
source: 'dto_schema/order/order.dsl.dto.ts',
|
|
84
|
-
resolver: (name) => ({ from: '@mall/enums/user', name }), // 枚举引用解析
|
|
85
|
-
});
|
|
10
|
+
project(地图:应用与 API 拓扑)
|
|
11
|
+
→ dictionary(词典:命名与数据类型标准)
|
|
12
|
+
→ prototype(概要设计:页面与字段)
|
|
13
|
+
→ table / dto(详细设计)
|
|
14
|
+
→ driver 生成产物(SQL / 枚举 / TypeBox)
|
|
86
15
|
```
|
|
87
16
|
|
|
88
|
-
##
|
|
17
|
+
## 文档
|
|
89
18
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
19
|
+
- [project.md](./docs/project.md) — 项目拓扑(地图)
|
|
20
|
+
- [dictionary.md](./docs/dictionary.md) — 短语词典(命名标准)
|
|
21
|
+
- [prototype.md](./docs/prototype.md) — 页面原型(概要设计)
|
|
22
|
+
- [table.md](./docs/table.md) — 定义表(详细设计)
|
|
23
|
+
- [dto.md](./docs/dto.md) — 定义 DTO
|
|
24
|
+
- [enum.md](./docs/enum.md) — 定义枚举
|
|
25
|
+
- [driver.md](./docs/driver.md) — driver 模式与产物生成
|
|
26
|
+
- [mysql-connection.md](./docs/mysql-connection.md) — MySQL 连接说明
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
/** A vocabulary entry. */
|
|
3
|
+
export interface DictionaryEntry extends SchemaBase {
|
|
4
|
+
/** Display label (Chinese) for the term. */
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Creates a phrase entry. */
|
|
8
|
+
export declare function definePhrase(extra: DictionaryEntry): DictionaryEntry;
|
package/dist/dsl.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DictionaryEntry } from './dictionary';
|
|
1
2
|
export interface SchemaBase {
|
|
2
3
|
name: string;
|
|
3
4
|
description?: string;
|
|
@@ -61,13 +62,19 @@ export interface EnumValue {
|
|
|
61
62
|
symbol: string;
|
|
62
63
|
label: string;
|
|
63
64
|
}
|
|
65
|
+
/** Shared enum definition. Pure value object, safe to reference from multiple fields/tables. */
|
|
66
|
+
export interface EnumDef {
|
|
67
|
+
/** JS 定义名称,如 MerchantStatus */
|
|
68
|
+
jsName: string;
|
|
69
|
+
valueType: 'string' | 'integer';
|
|
70
|
+
values: EnumValue[];
|
|
71
|
+
}
|
|
72
|
+
export declare function defineEnum(jsName: string, valueType: 'string' | 'integer', values: EnumValue[]): EnumDef;
|
|
64
73
|
export interface EnumField extends BaseField {
|
|
65
74
|
type: 'enum';
|
|
66
75
|
jsType: 'string' | 'number';
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
jsName?: string;
|
|
70
|
-
values: EnumValue[];
|
|
76
|
+
/** Reference to a shared enum definition (see defineEnum). */
|
|
77
|
+
enum: EnumDef;
|
|
71
78
|
}
|
|
72
79
|
interface JsonField extends BaseField {
|
|
73
80
|
type: 'json';
|
|
@@ -90,10 +97,14 @@ export interface TableSchema extends SchemaBase {
|
|
|
90
97
|
actor?: boolean;
|
|
91
98
|
/** id 生成器 */
|
|
92
99
|
generator?: string;
|
|
100
|
+
/** 自增主键字段 */
|
|
101
|
+
autoIncrement?: Field;
|
|
93
102
|
primaryKey?: Field | Field[];
|
|
94
103
|
indexes?: Index[];
|
|
95
104
|
/** 外键,引用其他表的字段 */
|
|
96
105
|
foreignKeys?: Record<string, ForeignKey>;
|
|
106
|
+
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
107
|
+
phrase?: DictionaryEntry;
|
|
97
108
|
fields: Record<string, Field>;
|
|
98
109
|
}
|
|
99
110
|
type FieldExtras<T extends Field> = Omit<T, 'name' | 'type' | 'jsType'>;
|
|
@@ -108,14 +119,17 @@ export declare function timeField(extra?: FieldExtras<TimeField>): TimeField;
|
|
|
108
119
|
export declare function datetimeField(extra?: FieldExtras<DateTimeField>): DateTimeField;
|
|
109
120
|
export declare function jsonField(extra?: FieldExtras<JsonField>): JsonField;
|
|
110
121
|
export declare function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField;
|
|
111
|
-
export declare function
|
|
122
|
+
export declare function defineTable(name: string, schema: {
|
|
112
123
|
description?: string;
|
|
113
124
|
paginated?: boolean;
|
|
114
125
|
actor?: boolean;
|
|
115
126
|
generator?: string;
|
|
127
|
+
autoIncrement?: Field;
|
|
116
128
|
primaryKey?: Field | Field[];
|
|
117
129
|
indexes?: Index[];
|
|
118
130
|
foreignKeys?: Record<string, ForeignKey>;
|
|
131
|
+
/** 引用的实体短语(词典条目) */
|
|
132
|
+
phrase?: DictionaryEntry;
|
|
119
133
|
fields: Record<string, Field>;
|
|
120
134
|
}): TableSchema;
|
|
121
135
|
export {};
|
package/dist/dsl.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// DSL field type definitions.
|
|
3
3
|
// Shape: { type: <type name>, <extension fields> }
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.defineEnum = defineEnum;
|
|
5
6
|
exports.stringField = stringField;
|
|
6
7
|
exports.textField = textField;
|
|
7
8
|
exports.intField = intField;
|
|
@@ -13,7 +14,10 @@ exports.timeField = timeField;
|
|
|
13
14
|
exports.datetimeField = datetimeField;
|
|
14
15
|
exports.jsonField = jsonField;
|
|
15
16
|
exports.enumField = enumField;
|
|
16
|
-
exports.
|
|
17
|
+
exports.defineTable = defineTable;
|
|
18
|
+
function defineEnum(jsName, valueType, values) {
|
|
19
|
+
return { jsName, valueType, values };
|
|
20
|
+
}
|
|
17
21
|
function stringField(extra = {}) {
|
|
18
22
|
return { name: '', type: 'string', jsType: 'string', ...extra };
|
|
19
23
|
}
|
|
@@ -45,22 +49,38 @@ function jsonField(extra = {}) {
|
|
|
45
49
|
return { name: '', type: 'json', jsType: 'object', ...extra };
|
|
46
50
|
}
|
|
47
51
|
function enumField(extra) {
|
|
48
|
-
const jsType = extra.valueType === 'integer' ? 'number' : 'string';
|
|
52
|
+
const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
|
|
49
53
|
return { name: '', type: 'enum', jsType, ...extra };
|
|
50
54
|
}
|
|
51
|
-
function
|
|
55
|
+
function defineTable(name, schema) {
|
|
52
56
|
const table = { name, ...schema };
|
|
57
|
+
for (const key of Object.keys(table.fields)) {
|
|
58
|
+
const field = table.fields[key];
|
|
59
|
+
if (field.schema && field.schema !== table) {
|
|
60
|
+
throw new Error(`field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
53
63
|
for (const key of Object.keys(table.fields)) {
|
|
54
64
|
table.fields[key].name = key;
|
|
55
65
|
table.fields[key].schema = table;
|
|
56
66
|
}
|
|
57
67
|
for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
|
|
58
68
|
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
59
|
-
|
|
69
|
+
const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
|
|
70
|
+
for (let i = 0; i < refs.length; i++) {
|
|
71
|
+
const ref = refs[i];
|
|
60
72
|
if (!ref.schema)
|
|
61
73
|
throw new Error(`foreign key ${fkName}: references field has no schema`);
|
|
62
74
|
if (ref.schema === table)
|
|
63
75
|
throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
|
|
76
|
+
const phrase = ref.schema.phrase;
|
|
77
|
+
if (!phrase)
|
|
78
|
+
throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
|
|
79
|
+
const expected = `${phrase.name}_${ref.name}`;
|
|
80
|
+
const fkField = fields[i];
|
|
81
|
+
if (fkField.name !== expected) {
|
|
82
|
+
throw new Error(`foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`);
|
|
83
|
+
}
|
|
64
84
|
}
|
|
65
85
|
}
|
|
66
86
|
return table;
|
package/dist/enum-driver.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function renderEnum(
|
|
1
|
+
import { EnumDef } from './dsl';
|
|
2
|
+
export declare function renderEnum(def: EnumDef): string;
|
package/dist/enum-driver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderEnum = renderEnum;
|
|
4
|
-
// Enum driver: renders an
|
|
4
|
+
// Enum driver: renders an EnumDef into a standalone TypeScript enum file.
|
|
5
5
|
// Shape matches the generated-enum product consumed by the TypeBox driver (Type.Enum):
|
|
6
6
|
//
|
|
7
7
|
// export enum UserStatus {
|
|
@@ -23,12 +23,10 @@ function renderValue(value) {
|
|
|
23
23
|
function labelName(jsName) {
|
|
24
24
|
return `${jsName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_LABEL`;
|
|
25
25
|
}
|
|
26
|
-
function renderEnum(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const members = field.values.map((v) => ` ${v.symbol} = ${renderValue(v.value)},`);
|
|
31
|
-
const labels = field.values.map((v) => ` [${name}.${v.symbol}]: ${renderString(v.label)},`);
|
|
26
|
+
function renderEnum(def) {
|
|
27
|
+
const name = def.jsName;
|
|
28
|
+
const members = def.values.map((v) => ` ${v.symbol} = ${renderValue(v.value)},`);
|
|
29
|
+
const labels = def.values.map((v) => ` [${name}.${v.symbol}]: ${renderString(v.label)},`);
|
|
32
30
|
return [
|
|
33
31
|
`export enum ${name} {`,
|
|
34
32
|
...members,
|
package/dist/flow.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
export interface FlowNode extends SchemaBase {
|
|
3
|
+
/** Optional sub-flow. When present, entering this node runs the sub-flow;
|
|
4
|
+
* after the sub-flow reaches any terminal node, the outer flow continues
|
|
5
|
+
* via this node's outgoing edges. Sub-flows nest recursively. */
|
|
6
|
+
flow?: FlowSchema;
|
|
7
|
+
}
|
|
8
|
+
export interface FlowEdge extends SchemaBase {
|
|
9
|
+
/** Trigger condition; undefined = default path (success/normal). */
|
|
10
|
+
when?: string;
|
|
11
|
+
start: FlowNode;
|
|
12
|
+
end: FlowNode;
|
|
13
|
+
}
|
|
14
|
+
export interface FlowSchema extends SchemaBase {
|
|
15
|
+
/** Entry node. */
|
|
16
|
+
start: FlowNode;
|
|
17
|
+
/** All nodes, collected from edges (deduplicated by object identity). */
|
|
18
|
+
nodes: FlowNode[];
|
|
19
|
+
/** Independent edges; a node may be start of many edges, so cycles are expressible. */
|
|
20
|
+
edges: FlowEdge[];
|
|
21
|
+
}
|
|
22
|
+
export declare function node(name: string, flow?: FlowSchema, description?: string): FlowNode;
|
|
23
|
+
export declare function edge(start: FlowNode, end: FlowNode, when?: string, description?: string): FlowEdge;
|
|
24
|
+
export declare function defineFlow(name: string, schema: {
|
|
25
|
+
start: FlowNode;
|
|
26
|
+
edges: FlowEdge[];
|
|
27
|
+
description?: string;
|
|
28
|
+
}): FlowSchema;
|
package/dist/flow.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.node = node;
|
|
4
|
+
exports.edge = edge;
|
|
5
|
+
exports.defineFlow = defineFlow;
|
|
6
|
+
function node(name, flow, description) {
|
|
7
|
+
return { name, flow, description };
|
|
8
|
+
}
|
|
9
|
+
function edge(start, end, when, description) {
|
|
10
|
+
// Auto name for uniformity with SchemaBase; `when` stays the branch marker.
|
|
11
|
+
return { name: `${start.name}->${end.name}`, start, end, when, description };
|
|
12
|
+
}
|
|
13
|
+
function defineFlow(name, schema) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const nodes = [];
|
|
16
|
+
for (const e of schema.edges) {
|
|
17
|
+
for (const n of [e.start, e.end]) {
|
|
18
|
+
if (!seen.has(n)) {
|
|
19
|
+
seen.add(n);
|
|
20
|
+
nodes.push(n);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!seen.has(schema.start)) {
|
|
25
|
+
seen.add(schema.start);
|
|
26
|
+
nodes.push(schema.start);
|
|
27
|
+
}
|
|
28
|
+
const flow = { name, description: schema.description, start: schema.start, nodes, edges: schema.edges };
|
|
29
|
+
validate(flow);
|
|
30
|
+
return flow;
|
|
31
|
+
}
|
|
32
|
+
// Nodes that never appear as an edge start are terminals. Reverse BFS from all
|
|
33
|
+
// terminals marks every node that can reach a terminal; unmarked nodes sit on
|
|
34
|
+
// a path that never ends (e.g. a cycle without an exit) — reject them at
|
|
35
|
+
// definition time.
|
|
36
|
+
function validate(schema) {
|
|
37
|
+
const starts = new Set();
|
|
38
|
+
for (const e of schema.edges)
|
|
39
|
+
starts.add(e.start);
|
|
40
|
+
const reverse = new Map();
|
|
41
|
+
for (const n of schema.nodes)
|
|
42
|
+
reverse.set(n, []);
|
|
43
|
+
for (const e of schema.edges) {
|
|
44
|
+
reverse.get(e.end).push(e.start);
|
|
45
|
+
}
|
|
46
|
+
const reached = new Set();
|
|
47
|
+
const queue = [];
|
|
48
|
+
for (const n of schema.nodes) {
|
|
49
|
+
if (!starts.has(n)) {
|
|
50
|
+
reached.add(n);
|
|
51
|
+
queue.push(n);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
while (queue.length > 0) {
|
|
55
|
+
const cur = queue.shift();
|
|
56
|
+
for (const prev of reverse.get(cur)) {
|
|
57
|
+
if (!reached.has(prev)) {
|
|
58
|
+
reached.add(prev);
|
|
59
|
+
queue.push(prev);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const n of schema.nodes) {
|
|
64
|
+
if (!reached.has(n)) {
|
|
65
|
+
throw new Error(`flow ${schema.name}: node "${n.name}" cannot reach a terminal`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export * from './dsl';
|
|
2
2
|
export * from './dto';
|
|
3
|
+
export * from './project';
|
|
4
|
+
export * from './prototype';
|
|
5
|
+
export * from './dictionary';
|
|
3
6
|
export * from './mysql-driver';
|
|
4
7
|
export * from './enum-driver';
|
|
5
8
|
export * from './typebox-driver';
|
|
9
|
+
export * from './pattern';
|
|
10
|
+
export * from './patterns/retry';
|
|
11
|
+
export * from './flow';
|
|
12
|
+
export * from './page';
|
|
13
|
+
export * from './page-flow';
|
|
14
|
+
export * from './mermaid-driver';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./dsl"), exports);
|
|
18
18
|
__exportStar(require("./dto"), exports);
|
|
19
|
+
__exportStar(require("./project"), exports);
|
|
20
|
+
__exportStar(require("./prototype"), exports);
|
|
21
|
+
__exportStar(require("./dictionary"), exports);
|
|
19
22
|
__exportStar(require("./mysql-driver"), exports);
|
|
20
23
|
__exportStar(require("./enum-driver"), exports);
|
|
21
24
|
__exportStar(require("./typebox-driver"), exports);
|
|
25
|
+
__exportStar(require("./pattern"), exports);
|
|
26
|
+
__exportStar(require("./patterns/retry"), exports);
|
|
27
|
+
__exportStar(require("./flow"), exports);
|
|
28
|
+
__exportStar(require("./page"), exports);
|
|
29
|
+
__exportStar(require("./page-flow"), exports);
|
|
30
|
+
__exportStar(require("./mermaid-driver"), exports);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderFlowMermaid = renderFlowMermaid;
|
|
4
|
+
exports.renderPageFlowMermaid = renderPageFlowMermaid;
|
|
5
|
+
// Mermaid driver: converts a FlowSchema into a Mermaid flowchart (TD).
|
|
6
|
+
// Node ids are hierarchical (n0, n0_0, n0_0_0, ...) so nested sub-flows stay
|
|
7
|
+
// globally unique. A node with a sub-flow renders as a subgraph block whose
|
|
8
|
+
// internals are rendered recursively. ok edges render as -->, conditional
|
|
9
|
+
// edges render as -->|"WHEN"|.
|
|
10
|
+
function escapeLabel(s) {
|
|
11
|
+
return s.replace(/"/g, '\\"').replace(/\n/g, '<br/>');
|
|
12
|
+
}
|
|
13
|
+
function renderFlowMermaid(schema) {
|
|
14
|
+
const lines = ['flowchart TD'];
|
|
15
|
+
const ids = new Map();
|
|
16
|
+
renderFlow(schema, 'n', lines, ids);
|
|
17
|
+
lines.push('');
|
|
18
|
+
lines.push(' classDef start fill:#e6f4ea,stroke:#333,stroke-width:1px;');
|
|
19
|
+
lines.push(` class ${ids.get(schema.start)} start;`);
|
|
20
|
+
return lines.join('\n');
|
|
21
|
+
}
|
|
22
|
+
function renderFlow(schema, prefix, lines, ids) {
|
|
23
|
+
schema.nodes.forEach((n, i) => ids.set(n, `${prefix}${i}`));
|
|
24
|
+
for (const n of schema.nodes) {
|
|
25
|
+
const id = ids.get(n);
|
|
26
|
+
if (n.flow) {
|
|
27
|
+
lines.push(` subgraph ${id}["${escapeLabel(n.name)}"]`);
|
|
28
|
+
renderFlow(n.flow, `${id}_`, lines, ids);
|
|
29
|
+
lines.push(' end');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
lines.push(` ${id}["${escapeLabel(n.name)}"]`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const e of schema.edges) {
|
|
36
|
+
lines.push(renderEdge(e, ids));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function renderEdge(e, ids) {
|
|
40
|
+
const label = e.when ? `|"${escapeLabel(e.when)}"|` : '';
|
|
41
|
+
return ` ${ids.get(e.start)} -->${label} ${ids.get(e.end)}`;
|
|
42
|
+
}
|
|
43
|
+
// Page-driven flow renderer: groups pages by their app into swimlane
|
|
44
|
+
// subgraphs, then renders edges across the whole flow.
|
|
45
|
+
function renderPageFlowMermaid(schema) {
|
|
46
|
+
const lines = ['flowchart TD'];
|
|
47
|
+
const ids = new Map();
|
|
48
|
+
const byApp = new Map();
|
|
49
|
+
for (const p of schema.pages) {
|
|
50
|
+
const list = byApp.get(p.app.name) ?? [];
|
|
51
|
+
list.push(p);
|
|
52
|
+
byApp.set(p.app.name, list);
|
|
53
|
+
}
|
|
54
|
+
let appIdx = 0;
|
|
55
|
+
for (const [appName, pages] of byApp) {
|
|
56
|
+
lines.push(` subgraph app${appIdx}["${escapeLabel(appName)}"]`);
|
|
57
|
+
pages.forEach((p, i) => ids.set(p, `app${appIdx}_p${i}`));
|
|
58
|
+
for (const p of pages) {
|
|
59
|
+
lines.push(` ${ids.get(p)}["${escapeLabel(p.name)}"]`);
|
|
60
|
+
}
|
|
61
|
+
appIdx++;
|
|
62
|
+
lines.push(' end');
|
|
63
|
+
}
|
|
64
|
+
for (const e of schema.edges) {
|
|
65
|
+
const label = e.when ? `|"${escapeLabel(e.when.name)}"|` : '';
|
|
66
|
+
lines.push(` ${ids.get(e.start)} -->${label} ${ids.get(e.end)}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(' classDef start fill:#e6f4ea,stroke:#333,stroke-width:1px;');
|
|
70
|
+
lines.push(` class ${ids.get(schema.start)} start;`);
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
package/dist/mysql-driver.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import { TableSchema } from './dsl';
|
|
2
|
-
export
|
|
2
|
+
export interface BuildCreateTableSqlOptions {
|
|
3
|
+
/** 是否生成外键约束,默认 false(不生成) */
|
|
4
|
+
generateForeignKeys?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function buildCreateTableSql(schema: TableSchema, options?: BuildCreateTableSqlOptions): string;
|
package/dist/mysql-driver.js
CHANGED
|
@@ -26,17 +26,19 @@ function columnType(field) {
|
|
|
26
26
|
return 'DATETIME';
|
|
27
27
|
case 'enum':
|
|
28
28
|
// Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT.
|
|
29
|
-
return field.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
|
|
29
|
+
return field.enum.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
|
|
30
30
|
case 'json':
|
|
31
31
|
return 'JSON';
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
function columnDef(field) {
|
|
34
|
+
function columnDef(field, autoIncrement) {
|
|
35
35
|
const parts = [field.name, columnType(field)];
|
|
36
36
|
if (field.optional === false)
|
|
37
37
|
parts.push('NOT NULL');
|
|
38
38
|
if (field.default !== undefined)
|
|
39
39
|
parts.push(`DEFAULT '${field.default}'`);
|
|
40
|
+
if (field === autoIncrement)
|
|
41
|
+
parts.push('AUTO_INCREMENT');
|
|
40
42
|
return parts.join(' ');
|
|
41
43
|
}
|
|
42
44
|
function primaryKeyClause(schema) {
|
|
@@ -51,12 +53,27 @@ function indexClause(index) {
|
|
|
51
53
|
const name = index.name ?? fields.map((f) => f.name).join('_');
|
|
52
54
|
return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
|
|
53
55
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
+
function foreignKeyClause(name, fk) {
|
|
57
|
+
const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
|
|
58
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
59
|
+
const refTable = refs[0].schema?.name;
|
|
60
|
+
if (!refTable)
|
|
61
|
+
throw new Error(`foreign key ${name}: references field has no schema`);
|
|
62
|
+
const fkCols = fields.map((f) => f.name).join(', ');
|
|
63
|
+
const refCols = refs.map((r) => r.name).join(', ');
|
|
64
|
+
return `CONSTRAINT \`${name}\` FOREIGN KEY (${fkCols}) REFERENCES \`${refTable}\` (${refCols})`;
|
|
65
|
+
}
|
|
66
|
+
function buildCreateTableSql(schema, options = {}) {
|
|
67
|
+
const lines = Object.values(schema.fields).map((field) => columnDef(field, schema.autoIncrement));
|
|
56
68
|
const pk = primaryKeyClause(schema);
|
|
57
69
|
if (pk)
|
|
58
70
|
lines.push(pk);
|
|
59
71
|
for (const index of schema.indexes ?? [])
|
|
60
72
|
lines.push(indexClause(index));
|
|
73
|
+
if (options.generateForeignKeys) {
|
|
74
|
+
for (const [name, fk] of Object.entries(schema.foreignKeys ?? {})) {
|
|
75
|
+
lines.push(foreignKeyClause(name, fk));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
61
78
|
return `CREATE TABLE \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
|
|
62
79
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
import { ActionSchema, Page } from './page';
|
|
3
|
+
export interface PageEdge extends SchemaBase {
|
|
4
|
+
/** Trigger action; undefined = default path (success/normal). */
|
|
5
|
+
when?: ActionSchema;
|
|
6
|
+
start: Page;
|
|
7
|
+
end: Page;
|
|
8
|
+
}
|
|
9
|
+
export interface PageFlow extends SchemaBase {
|
|
10
|
+
/** Entry page. */
|
|
11
|
+
start: Page;
|
|
12
|
+
/** All pages, collected from edges (deduplicated by object identity). */
|
|
13
|
+
pages: Page[];
|
|
14
|
+
edges: PageEdge[];
|
|
15
|
+
}
|
|
16
|
+
export declare function pageEdge(start: Page, end: Page, when?: ActionSchema, description?: string): PageEdge;
|
|
17
|
+
export declare function definePageFlow(name: string, schema: {
|
|
18
|
+
start: Page;
|
|
19
|
+
edges: PageEdge[];
|
|
20
|
+
description?: string;
|
|
21
|
+
}): PageFlow;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pageEdge = pageEdge;
|
|
4
|
+
exports.definePageFlow = definePageFlow;
|
|
5
|
+
function pageEdge(start, end, when, description) {
|
|
6
|
+
// Auto name for uniformity with SchemaBase; `when` stays the branch marker.
|
|
7
|
+
return { name: `${start.name}->${end.name}`, start, end, when, description };
|
|
8
|
+
}
|
|
9
|
+
function definePageFlow(name, schema) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const pages = [];
|
|
12
|
+
for (const e of schema.edges) {
|
|
13
|
+
for (const p of [e.start, e.end]) {
|
|
14
|
+
if (!seen.has(p)) {
|
|
15
|
+
seen.add(p);
|
|
16
|
+
pages.push(p);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!seen.has(schema.start)) {
|
|
21
|
+
seen.add(schema.start);
|
|
22
|
+
pages.push(schema.start);
|
|
23
|
+
}
|
|
24
|
+
return { name, description: schema.description, start: schema.start, pages, edges: schema.edges };
|
|
25
|
+
}
|
package/dist/page.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
import { FrontApp } from './project';
|
|
3
|
+
/** Standalone page definition. A page is a shared value object: it lists
|
|
4
|
+
* the actions a user can perform, and belongs to exactly one frontend app. */
|
|
5
|
+
export interface PageSchema extends SchemaBase {
|
|
6
|
+
/** The frontend app this page belongs to (shared instance from project.config). */
|
|
7
|
+
app: FrontApp;
|
|
8
|
+
/** Actions a user can perform on this page (e.g. submit, approve, reject). */
|
|
9
|
+
actions: ActionSchema[];
|
|
10
|
+
}
|
|
11
|
+
/** An action a user can perform on a page (e.g. submit, approve, reject). */
|
|
12
|
+
export interface ActionSchema extends SchemaBase {
|
|
13
|
+
}
|
|
14
|
+
export declare function defineAction(name: string, description?: string): ActionSchema;
|
|
15
|
+
export declare function definePage(schema: {
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
app: FrontApp;
|
|
19
|
+
actions: ActionSchema[];
|
|
20
|
+
}): PageSchema;
|
|
21
|
+
/** A page node in a page-driven flow: every node is a page, and a page belongs to an app. */
|
|
22
|
+
export interface Page extends SchemaBase {
|
|
23
|
+
/** The frontend app this page belongs to (shared instance from project.config). */
|
|
24
|
+
app: FrontApp;
|
|
25
|
+
}
|
|
26
|
+
export declare function page(app: FrontApp, name: string, description?: string): Page;
|
package/dist/page.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineAction = defineAction;
|
|
4
|
+
exports.definePage = definePage;
|
|
5
|
+
exports.page = page;
|
|
6
|
+
function defineAction(name, description) {
|
|
7
|
+
return { name, description };
|
|
8
|
+
}
|
|
9
|
+
function definePage(schema) {
|
|
10
|
+
return { ...schema };
|
|
11
|
+
}
|
|
12
|
+
function page(app, name, description) {
|
|
13
|
+
return { name, app, description };
|
|
14
|
+
}
|