@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.
Files changed (51) hide show
  1. package/README.md +17 -85
  2. package/dist/dictionary.d.ts +8 -0
  3. package/dist/dictionary.js +7 -0
  4. package/dist/dsl.d.ts +19 -5
  5. package/dist/dsl.js +24 -4
  6. package/dist/enum-driver.d.ts +2 -2
  7. package/dist/enum-driver.js +5 -7
  8. package/dist/flow.d.ts +28 -0
  9. package/dist/flow.js +68 -0
  10. package/dist/index.d.ts +9 -0
  11. package/dist/index.js +9 -0
  12. package/dist/mermaid-driver.d.ts +4 -0
  13. package/dist/mermaid-driver.js +72 -0
  14. package/dist/mysql-driver.d.ts +5 -1
  15. package/dist/mysql-driver.js +21 -4
  16. package/dist/page-flow.d.ts +21 -0
  17. package/dist/page-flow.js +25 -0
  18. package/dist/page.d.ts +26 -0
  19. package/dist/page.js +14 -0
  20. package/dist/pattern.d.ts +15 -0
  21. package/dist/pattern.js +13 -0
  22. package/dist/patterns/retry.d.ts +16 -0
  23. package/dist/patterns/retry.js +40 -0
  24. package/dist/project.d.ts +41 -0
  25. package/dist/project.js +11 -0
  26. package/dist/prototype.d.ts +18 -0
  27. package/dist/prototype.js +10 -0
  28. package/dist/typebox-driver.js +4 -8
  29. package/docs/dictionary.md +20 -0
  30. package/docs/driver.md +42 -0
  31. package/docs/dto.md +54 -0
  32. package/docs/enum.md +24 -0
  33. package/docs/mysql-connection.md +1 -0
  34. package/docs/project.md +24 -0
  35. package/docs/prototype.md +21 -0
  36. package/docs/table.md +90 -0
  37. package/package.json +3 -2
  38. package/src/dictionary.ts +15 -0
  39. package/src/dsl.ts +50 -8
  40. package/src/enum-driver.ts +7 -8
  41. package/src/flow.ts +104 -0
  42. package/src/index.ts +9 -0
  43. package/src/mermaid-driver.ts +81 -0
  44. package/src/mysql-driver.ts +26 -5
  45. package/src/page-flow.ts +52 -0
  46. package/src/page.ts +40 -0
  47. package/src/pattern.ts +27 -0
  48. package/src/patterns/retry.ts +55 -0
  49. package/src/project.ts +55 -0
  50. package/src/prototype.ts +30 -0
  51. 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),再由 driver 翻译成目标产物(SQL、TS 枚举、TypeBox 源码)。
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
- ### 定义 DTO(四种方向)
53
-
54
- ```ts
55
- import { buildInput, buildPk, buildQuery, dtoField, from } from './src/dto';
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
- - **字段两层名**:`DtoField.name` 是接口字段名(DTO 容器 map key 反写);`field.name` 是数据库列名(buildTable 反写)。`from()` 提取的字段共享表 Field 实例,`name/schema` 保持指向表,不会被 DTO 覆盖。
91
- - **可选性优先级**:DTO `optional` 优先于字段层 `optional`;query 方向所有字段强制可选(Rule B)。
92
- - **枚举外部引用**:枚举由 enum-driver 生成独立文件,typebox-driver 只渲染 `Type.Enum(名称)` + import 引用,不内联。
93
- - **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
94
- - **继承分页**:`.include({ from, name, args })` 渲染 `Type.Intersect([PageRequest, Type.Object({…})])`。
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;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.definePhrase = definePhrase;
4
+ /** Creates a phrase entry. */
5
+ function definePhrase(extra) {
6
+ return extra;
7
+ }
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
- valueType: 'string' | 'integer';
68
- /** JS 定义名称,如 MerchantStatus */
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 buildTable(name: string, schema: {
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.buildTable = buildTable;
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 buildTable(name, schema) {
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
- for (const ref of refs) {
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;
@@ -1,2 +1,2 @@
1
- import { EnumField } from './dsl';
2
- export declare function renderEnum(field: EnumField): string;
1
+ import { EnumDef } from './dsl';
2
+ export declare function renderEnum(def: EnumDef): string;
@@ -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 EnumField into a standalone TypeScript enum file.
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(field) {
27
- if (!field.jsName)
28
- throw new Error(`enum field ${field.name} requires jsName to render`);
29
- const name = field.jsName;
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,4 @@
1
+ import { FlowSchema } from './flow';
2
+ import { PageFlow } from './page-flow';
3
+ export declare function renderFlowMermaid(schema: FlowSchema): string;
4
+ export declare function renderPageFlowMermaid(schema: PageFlow): string;
@@ -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
+ }
@@ -1,2 +1,6 @@
1
1
  import { TableSchema } from './dsl';
2
- export declare function buildCreateTableSql(schema: TableSchema): string;
2
+ export interface BuildCreateTableSqlOptions {
3
+ /** 是否生成外键约束,默认 false(不生成) */
4
+ generateForeignKeys?: boolean;
5
+ }
6
+ export declare function buildCreateTableSql(schema: TableSchema, options?: BuildCreateTableSqlOptions): string;
@@ -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 buildCreateTableSql(schema) {
55
- const lines = Object.values(schema.fields).map(columnDef);
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
+ }