@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
@@ -0,0 +1,15 @@
1
+ export interface PatternParamDef {
2
+ type: 'int' | 'string';
3
+ min?: number;
4
+ default?: number | string;
5
+ }
6
+ export interface PatternDef {
7
+ name: string;
8
+ params: Record<string, PatternParamDef>;
9
+ }
10
+ export interface PatternRef {
11
+ ref: string;
12
+ args: Record<string, unknown>;
13
+ }
14
+ export declare function definePattern(def: PatternDef): PatternDef;
15
+ export declare function ref(name: string, args: Record<string, unknown>): PatternRef;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ // Pattern core: minimal definitions to bootstrap the Flow × Pattern DSL.
3
+ // A Pattern is a reusable solution ("how to guarantee success") declared as
4
+ // pure data. Concrete usage fills its params and action injection points.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.definePattern = definePattern;
7
+ exports.ref = ref;
8
+ function definePattern(def) {
9
+ return def;
10
+ }
11
+ function ref(name, args) {
12
+ return { ref: name, args };
13
+ }
@@ -0,0 +1,16 @@
1
+ import { PatternDef } from '../pattern';
2
+ export declare const retryPattern: PatternDef;
3
+ export interface RetryAction {
4
+ /** Function to call, e.g. 'queryOrderList' */
5
+ call: string;
6
+ /** Type name of the single argument passed through, e.g. 'OrderQueryParams' */
7
+ params?: string;
8
+ }
9
+ export interface RetryRefArgs {
10
+ max?: number;
11
+ backoffMs?: number;
12
+ action: RetryAction;
13
+ /** Generated function name; defaults to '<call>WithRetry' */
14
+ fnName?: string;
15
+ }
16
+ export declare function renderRetry(args: RetryRefArgs): string;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.retryPattern = void 0;
4
+ exports.renderRetry = renderRetry;
5
+ // Retry pattern: blind mechanical retry for read-only operations.
6
+ // Valid because a read-only action is idempotent by nature: clicking a query
7
+ // button any number of times never changes the result, so retrying the same
8
+ // call is always safe. No idempotency key, no query-and-resume, no compensate.
9
+ exports.retryPattern = {
10
+ name: 'retry',
11
+ params: {
12
+ max: { type: 'int', min: 1, default: 3 },
13
+ backoffMs: { type: 'int', min: 0, default: 0 },
14
+ },
15
+ };
16
+ function renderRetry(args) {
17
+ if (args.max !== undefined && args.max < 1)
18
+ throw new Error('retry: max must be >= 1');
19
+ if (!args.action.call)
20
+ throw new Error('retry: action.call is required');
21
+ const max = args.max ?? 3;
22
+ const backoffMs = args.backoffMs ?? 0;
23
+ const call = args.action.call;
24
+ const paramType = args.action.params ?? 'unknown';
25
+ const fnName = args.fnName ?? `${call}WithRetry`;
26
+ const retryLine = backoffMs > 0 ? ` await sleep(${backoffMs});` : '';
27
+ return [
28
+ `export async function ${fnName}(params: ${paramType}) {`,
29
+ ` for (let attempt = 1; ; attempt++) {`,
30
+ ` try {`,
31
+ ` return await ${call}(params);`,
32
+ ` } catch (err) {`,
33
+ ` if (attempt >= ${max}) throw err;`,
34
+ retryLine,
35
+ ` }`,
36
+ ` }`,
37
+ `}`,
38
+ '',
39
+ ].join('\n');
40
+ }
@@ -0,0 +1,41 @@
1
+ import { SchemaBase } from './dsl';
2
+ /** Frontend form factor. Closed enum, extend when new form factors appear. */
3
+ export type FrontType = 'admin' | 'wxmini';
4
+ /** A frontend application (e.g. admin console, wechat mini program). */
5
+ export interface FrontApp extends SchemaBase {
6
+ type: FrontType;
7
+ /** Source directory relative to project root, e.g. 'web-admin/'. */
8
+ dir: string;
9
+ }
10
+ /** A backend API service. apps references shared FrontApp instances. */
11
+ export interface ProjectApi extends SchemaBase {
12
+ /** Source directory relative to project root, e.g. 'api/'. */
13
+ dir: string;
14
+ /** Frontends this API serves. Direct instance references (see defineProject). */
15
+ apps: FrontApp[];
16
+ /** API base URL prefix shared by all apps it serves, e.g. '/mall'. '' = no prefix. */
17
+ contextPath?: string;
18
+ }
19
+ /** A third-party system (e.g. wechat pay, unionpay). Owns its own
20
+ * implementation dir and contract (controller_types), just like an API,
21
+ * but is not part of this repo's served surface. */
22
+ export interface ThirdApi extends SchemaBase {
23
+ /** Source directory relative to project root, e.g. 'wechat/'. */
24
+ dir: string;
25
+ }
26
+ export interface ProjectSchema extends SchemaBase {
27
+ apps: FrontApp[];
28
+ apis: ProjectApi[];
29
+ thirdApis: ThirdApi[];
30
+ }
31
+ /**
32
+ * Defines the project topology. FrontApp instances are shared value objects:
33
+ * api.apps references the same instances from project.apps, so an app served
34
+ * by multiple APIs is defined once and referenced many times.
35
+ */
36
+ export declare function defineProject(name: string, schema: {
37
+ description?: string;
38
+ apps: FrontApp[];
39
+ apis: ProjectApi[];
40
+ thirdApis?: ThirdApi[];
41
+ }): ProjectSchema;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineProject = defineProject;
4
+ /**
5
+ * Defines the project topology. FrontApp instances are shared value objects:
6
+ * api.apps references the same instances from project.apps, so an app served
7
+ * by multiple APIs is defined once and referenced many times.
8
+ */
9
+ function defineProject(name, schema) {
10
+ return { name, ...schema, thirdApis: schema.thirdApis ?? [] };
11
+ }
@@ -0,0 +1,18 @@
1
+ import { SchemaBase } from './dsl';
2
+ /** Display metadata for a prototype field. */
3
+ export interface PrototypeFieldMeta {
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ export interface PrototypeSchema extends SchemaBase {
8
+ /** Field requirements: key is the field name, value is display metadata. */
9
+ fields: Record<string, PrototypeFieldMeta>;
10
+ }
11
+ /**
12
+ * Defines a page prototype. The field keys become the names referenced by
13
+ * later DTO/table definitions; here they only carry label/description.
14
+ */
15
+ export declare function definePrototype(name: string, schema: {
16
+ description?: string;
17
+ fields: Record<string, PrototypeFieldMeta>;
18
+ }): PrototypeSchema;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.definePrototype = definePrototype;
4
+ /**
5
+ * Defines a page prototype. The field keys become the names referenced by
6
+ * later DTO/table definitions; here they only carry label/description.
7
+ */
8
+ function definePrototype(name, schema) {
9
+ return { name, ...schema };
10
+ }
@@ -43,11 +43,9 @@ function renderBasic(field, pattern, resolver) {
43
43
  case 'json':
44
44
  return 'Type.Unknown()';
45
45
  case 'enum': {
46
- if (!field.jsName)
47
- throw new Error(`enum field ${field.name} requires jsName to render`);
48
- const ref = resolver?.(field.jsName);
46
+ const ref = resolver?.(field.enum.jsName);
49
47
  if (!ref)
50
- throw new Error(`enum field ${field.name}: no import ref for ${field.jsName} — pass an EnumResolver`);
48
+ throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
51
49
  return `Type.Enum(${ref.name})`;
52
50
  }
53
51
  default:
@@ -85,11 +83,9 @@ function collectEnumImports(f, resolver, out) {
85
83
  return;
86
84
  }
87
85
  if (f.field.type === 'enum') {
88
- if (!f.field.jsName)
89
- throw new Error(`enum field ${f.field.name} requires jsName to render`);
90
- const ref = resolver?.(f.field.jsName);
86
+ const ref = resolver?.(f.field.enum.jsName);
91
87
  if (!ref)
92
- throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.jsName} — pass an EnumResolver`);
88
+ throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
93
89
  out.set(`${ref.from}#${ref.name}`, ref);
94
90
  }
95
91
  }
@@ -0,0 +1,20 @@
1
+ # 短语词典 (Dictionary)
2
+
3
+ 词典是与团队达成共识的基础知识库:**某词代表什么**(语义/定义层面),不是物理形式。短语定了,字段命名、外键命名就都有依据——全项目只说同一种话。
4
+
5
+ - 是基础知识库,很少变更。
6
+ - **能引用就引用**:魔法字符串只在首次出现时使用,之后一律引用词典条目。
7
+
8
+ ## 定义
9
+
10
+ ```ts
11
+ import { definePhrase } from '@pylonts/dsl';
12
+
13
+ const BD = definePhrase({ label: 'BD推广员', description: '线下拓展商户、辅助入驻的推广人员' });
14
+ const Amt = definePhrase({ label: '金额', description: '交易金额,单位分' });
15
+ ```
16
+
17
+ ## 使用
18
+
19
+ - **表链接实体**:`TableSchema.phrase` 引用实体条目,声明本表归属哪个实体(见 [table.md](./table.md) 的外键检查链)。关联表等多实体场景不需要。
20
+ - 业务短语供字段命名/文档使用,跨团队对齐。
package/docs/driver.md ADDED
@@ -0,0 +1,42 @@
1
+ # Driver 模式与产物生成
2
+
3
+ DSL 定义元数据,driver 翻译成目标语言产物。产物与定义解耦,同一份定义可生成不同目标:
4
+
5
+ | 定义 | Driver | 产物 | 消费方 |
6
+ |---|---|---|---|
7
+ | `TableSchema` | mysql-driver | `CREATE TABLE` | MySQL |
8
+ | `EnumDef` | enum-driver | `export enum Xxx { … }` + `XXX_LABEL` | 业务代码 |
9
+ | `DtoMessage` | typebox-driver | `Type.Object({…})` + `Static` 推导 | fastify v5 参数校验 |
10
+
11
+ ## 生成 SQL
12
+
13
+ ```ts
14
+ import { buildCreateTableSql } from '@pylonts/dsl';
15
+
16
+ buildCreateTableSql(order); // "CREATE TABLE `order` (\n ..."
17
+ ```
18
+
19
+ 默认不生成外键约束;需要时:
20
+
21
+ ```ts
22
+ buildCreateTableSql(order, { generateForeignKeys: true });
23
+ ```
24
+
25
+ ## 生成枚举源码
26
+
27
+ ```ts
28
+ import { renderEnum } from '@pylonts/dsl';
29
+
30
+ renderEnum(AcquiringType); // TS enum 源码
31
+ ```
32
+
33
+ ## 生成 TypeBox 源码
34
+
35
+ ```ts
36
+ import { renderDtoMessage } from '@pylonts/dsl';
37
+
38
+ renderDtoMessage(orderPageQuery, {
39
+ source: 'dto_schema/order/order.dsl.dto.ts',
40
+ resolver: (name) => ({ from: '@mall/enums/user', name }), // 枚举引用解析
41
+ });
42
+ ```
package/docs/dto.md ADDED
@@ -0,0 +1,54 @@
1
+ # 定义 DTO(四种方向)
2
+
3
+ DTO 描述接口出入参。方向决定语义与可选性规则:
4
+
5
+ | 构建器 | 方向 | 用途 |
6
+ |---|---|---|
7
+ | `buildInput` | input | 新增/修改请求体 |
8
+ | `buildOutput` | output | 响应体 |
9
+ | `buildQuery` | query | 分页 + 过滤查询(字段恒为可选) |
10
+ | `buildPk` | pk | 按主键取详情 |
11
+
12
+ ## 从表提取字段
13
+
14
+ ```ts
15
+ import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';
16
+
17
+ // 输入:新增订单
18
+ buildInput('OrderAddRequest', { ...from(order, [order.fields.merchant_id, order.fields.amount]) });
19
+
20
+ // 输出:订单行
21
+ buildOutput('OrderRow', from(order, [order.fields.id, order.fields.order_no]));
22
+
23
+ // 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
24
+ buildQuery('OrderPageQuery', {
25
+ keyword: dtoField(stringField({ maxLength: 32 })).op('like'),
26
+ ...from(order, [order.fields.merchant_id]),
27
+ });
28
+
29
+ // 主键:按 id 取详情
30
+ buildPk('OrderDetailRequest', from(order, [order.fields.id]));
31
+ ```
32
+
33
+ `from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema` 保持指向表。
34
+
35
+ ## 独立字段
36
+
37
+ 不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`。
38
+
39
+ ```ts
40
+ dtoField(stringField({ maxLength: 32 })).op('like')
41
+ ```
42
+
43
+ ## 继承基础 schema
44
+
45
+ ```ts
46
+ buildQuery('OrderPageQuery', { ... })
47
+ .include({ from: '@pylonts/core', name: 'PageRequest' }); // 渲染 Type.Intersect([PageRequest, ...])
48
+ ```
49
+
50
+ ## 关键语义
51
+
52
+ - **字段两层名**:`DtoField.name` 是接口字段名(DTO map key 反写);`field.name` 是数据库列名(表反写)。
53
+ - **可选性优先级**:DTO 层 `optional` 优先于字段层;query 方向所有字段强制可选。
54
+ - **HTTP 传 string**:bigint / decimal / date / time 在接口层渲染为 `Type.String()`,保证精度与序列化语义。
package/docs/enum.md ADDED
@@ -0,0 +1,24 @@
1
+ # 定义枚举(可跨表复用)
2
+
3
+ 枚举定义与字段分离:`defineEnum` 产生共享定义(纯值对象),`enumField` 引用它。同一枚举可被多张表 / 多个 DTO 复用,只生成一次。
4
+
5
+ ```ts
6
+ import { defineEnum, enumField } from '@pylonts/dsl';
7
+
8
+ // 共享定义(_common.ts 等公共文件)
9
+ export const AcquiringType = defineEnum('AcquiringType', 'string', [
10
+ { symbol: 'WECHAT', value: 'wechat', label: '微信' },
11
+ { symbol: 'UNIONPAY', value: 'unionpay', label: '银联商务' },
12
+ ]);
13
+
14
+ // 字段引用(每表独立实例)
15
+ buildTable('merchant', { fields: { acquiring_type: enumField({ enum: AcquiringType }) }, ... });
16
+ buildTable('order', { fields: { acquiring_type: enumField({ enum: AcquiringType }) }, ... });
17
+ ```
18
+
19
+ - 字段实例每表独立(列名、可选性随表),枚举定义全局共享。
20
+ - 枚举由 enum-driver 生成独立文件;typebox-driver 只渲染 `Type.Enum(名称)` + import 引用,不内联。
21
+
22
+ ## 生成枚举源码
23
+
24
+ 见 [driver.md](./driver.md)。
@@ -0,0 +1 @@
1
+ 连接 MySQL 时,必须配置:`supportBigNumbers: true, bigNumberStrings: true`
@@ -0,0 +1,24 @@
1
+ # 项目拓扑 (Project)
2
+
3
+ Project 是仓库的地图:描述有哪些前端应用、哪些后端 API,以及每个 API 服务哪些前端。
4
+
5
+ ```ts
6
+ import { defineProject } from '@pylonts/dsl';
7
+
8
+ const webAdmin = { name: 'web-admin', type: 'admin', dir: 'web-admin/', description: '管理后台' };
9
+ const miniUser = { name: 'mini-user', type: 'wxmini', dir: 'mini-user/', description: 'C端小程序' };
10
+ const miniVerify = { name: 'mini-verify', type: 'wxmini', dir: 'mini-verify/',description: '核销小程序' };
11
+
12
+ export const mall = defineProject('mall', {
13
+ description: '合作商户权益兑换商城',
14
+ apps: [webAdmin, miniUser, miniVerify],
15
+ apis: [
16
+ { name: 'mall-api', description: '商城主后端', dir: 'api/', contextPath: '/mall', apps: [webAdmin, miniUser, miniVerify] },
17
+ ],
18
+ });
19
+ ```
20
+
21
+ - `FrontApp`:`name` / `description` / `type`(admin | wxmini)/ `dir`(相对仓库根目录的源码目录)。
22
+ - `ProjectApi`:`name` / `description` / `dir` / `apps`(直接引用共享的 FrontApp 实例——一个 app 被多个 API 服务就定义一次、引用多次)/ `contextPath`(API 基础 URL 前缀,如 `/mall`,空串表示无前缀)。
23
+ - **直接对象引用优先**:`api.apps` 与 `project.apps` 指向同一实例,不写字符串。
24
+ - **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
@@ -0,0 +1,21 @@
1
+ # 页面原型 (Prototype)
2
+
3
+ 原型是**单个实例**的页面/功能概要设计——一张页面的草图。只列出页面需要的字段,不涉及类型、不绑定 app/API/表,字段细节(DTO/表定义)在详细设计阶段另行编写、后续连接。
4
+
5
+ ```ts
6
+ import { definePrototype } from '@pylonts/dsl';
7
+
8
+ export const adminOrderList = definePrototype('AdminOrderList', {
9
+ description: '管理端订单列表页',
10
+ fields: {
11
+ order_no: { label: '订单号', description: '商家下单生成的订单编号' },
12
+ merchant: { label: '商户', description: '下单商户' },
13
+ amount: { label: '金额', description: '订单实付金额' },
14
+ created_at: { label: '下单时间' },
15
+ },
16
+ });
17
+ ```
18
+
19
+ - 字段 key 即字段名,后续 DTO/表定义直接引用同名;这里只携带 `label` / `description`。
20
+ - 原型是单实例的(一个东西的原型,不是整个项目的),同名页面在不同 app 中各自定义,互不干扰。
21
+ - 不做回写:原型字段无写回机制,各走各的。
package/docs/table.md ADDED
@@ -0,0 +1,90 @@
1
+ # 定义表 (TableSchema)
2
+
3
+ ## 字段类型
4
+
5
+ | 构建器 | 类型 | jsType | MySQL 列 | 备注 |
6
+ |---|---|---|---|---|
7
+ | `stringField` | string | string | VARCHAR | 必填 `maxLength` |
8
+ | `textField` | text | string | TEXT | |
9
+ | `intField` | integer | number | INT | |
10
+ | `bigintField` | bigint | string | BIGINT | 传输层走 string 保精度 |
11
+ | `decimalField` | decimal | string | DECIMAL | 必填 `precision` / `scale`,传输层走 string 避免浮点误差 |
12
+ | `booleanField` | boolean | boolean | TINYINT(1) | |
13
+ | `dateField` | date | Date | DATE | |
14
+ | `timeField` | time | string | TIME | |
15
+ | `datetimeField` | datetime | Date | DATETIME | |
16
+ | `enumField` | enum | string / number | VARCHAR(20) / TINYINT | 引用共享枚举定义,见 [enum.md](./enum.md) |
17
+ | `jsonField` | json | object | JSON | |
18
+
19
+ 通用扩展属性(构建器第二参数):`label`(中文标签)、`description`、`optional`、`readOnly`、`default`。
20
+
21
+ ## 定义表
22
+
23
+ ```ts
24
+ import { bigintField, defineTable, decimalField, stringField } from '@pylonts/dsl';
25
+
26
+ const id = bigintField({ readOnly: true, label: '主键' });
27
+
28
+ export const order = defineTable('order', {
29
+ description: '订单',
30
+ generator: 'auto_increment',
31
+ fields: {
32
+ id,
33
+ order_no: stringField({ label: '订单号', maxLength: 32, optional: false }),
34
+ amount: decimalField({ precision: 18, scale: 2, label: '金额' }),
35
+ },
36
+ primaryKey: id,
37
+ });
38
+ ```
39
+
40
+ - 字段名从 map key 反写,`fields` 里的 key 就是列名。
41
+ - 字段实例不可跨表复用(复用同一字段实例会抛错),枚举除外。
42
+
43
+ ## 索引
44
+
45
+ ```ts
46
+ indexes: [
47
+ { name: 'uk_uuid', fields: c_uuid, unique: true },
48
+ { fields: [c_enum, c_date] }, // 名字缺省时 = 字段名 join '_'
49
+ ],
50
+ ```
51
+
52
+ ## 外键与短语检查链
53
+
54
+ ```ts
55
+ import { definePhrase } from '@pylonts/dsl';
56
+
57
+ const BD = definePhrase({ label: 'BD推广员', description: '线下拓展商户的推广人员' });
58
+
59
+ const bdId = bigintField({ readOnly: true, label: 'BD ID' });
60
+
61
+ export const bd = defineTable('bd', {
62
+ description: 'BD',
63
+ phrase: BD, // 链接词典条目:本表归属的实体
64
+ fields: { id: bdId },
65
+ primaryKey: bdId,
66
+ });
67
+
68
+ const auditBdId = bigintField({ label: 'BD' });
69
+
70
+ export const audit = defineTable('audit', {
71
+ description: '审核',
72
+ fields: {
73
+ bd_id: auditBdId, // 列名必须 = phrase.name + '_' + 被引用字段名
74
+ },
75
+ foreignKeys: {
76
+ bd_bd_id: { fields: auditBdId, references: bdId },
77
+ },
78
+ });
79
+ ```
80
+
81
+ **规则(defineTable 时强制检查)**:外键字段名必须等于 `被引用表.phrase.name + "_" + 被引用字段名`。即引用 `bd.id` 的字段必须叫 `bd_id`——`bd` 来自词典(权威短语),`id` 是 `bd` 表主键。
82
+
83
+ - 被引用表未定义 `phrase` → 抛错(检查链要求每个被引用表都有短语)。
84
+ - 命名不匹配 → 抛错并提示期望名,例如:
85
+ `foreign key bad: field must be named bd_id (phrase bd + id), got merchant_id`
86
+ - 关联表等涉及多个实体的场景不需要 `phrase`,也不建外键。
87
+
88
+ ## 生成 SQL
89
+
90
+ 见 [driver.md](./driver.md)。
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.ts",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "src"
10
+ "src",
11
+ "docs"
11
12
  ],
12
13
  "scripts": {
13
14
  "build": "tsc -p tsconfig.build.json",
@@ -0,0 +1,15 @@
1
+ import { SchemaBase } from './dsl';
2
+
3
+ // Dictionary definitions: vocabulary shared across the team — what a term
4
+ // means and what it is called.
5
+
6
+ /** A vocabulary entry. */
7
+ export interface DictionaryEntry extends SchemaBase {
8
+ /** Display label (Chinese) for the term. */
9
+ label?: string;
10
+ }
11
+
12
+ /** Creates a phrase entry. */
13
+ export function definePhrase(extra: DictionaryEntry): DictionaryEntry {
14
+ return extra;
15
+ }
package/src/dsl.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // DSL field type definitions.
2
2
  // Shape: { type: <type name>, <extension fields> }
3
3
 
4
+ import type { DictionaryEntry } from './dictionary';
5
+
4
6
  export interface SchemaBase {
5
7
  name: string;
6
8
  description?: string;
@@ -78,13 +80,27 @@ export interface EnumValue {
78
80
  label: string;
79
81
  }
80
82
 
83
+ /** Shared enum definition. Pure value object, safe to reference from multiple fields/tables. */
84
+ export interface EnumDef {
85
+ /** JS 定义名称,如 MerchantStatus */
86
+ jsName: string;
87
+ valueType: 'string' | 'integer';
88
+ values: EnumValue[];
89
+ }
90
+
91
+ export function defineEnum(
92
+ jsName: string,
93
+ valueType: 'string' | 'integer',
94
+ values: EnumValue[],
95
+ ): EnumDef {
96
+ return { jsName, valueType, values };
97
+ }
98
+
81
99
  export interface EnumField extends BaseField {
82
100
  type: 'enum';
83
101
  jsType: 'string' | 'number';
84
- valueType: 'string' | 'integer';
85
- /** JS 定义名称,如 MerchantStatus */
86
- jsName?: string;
87
- values: EnumValue[];
102
+ /** Reference to a shared enum definition (see defineEnum). */
103
+ enum: EnumDef;
88
104
  }
89
105
 
90
106
  interface JsonField extends BaseField {
@@ -123,15 +139,19 @@ export interface TableSchema extends SchemaBase {
123
139
  actor?: boolean;
124
140
  /** id 生成器 */
125
141
  generator?: string;
142
+ /** 自增主键字段 */
143
+ autoIncrement?: Field;
126
144
  primaryKey?: Field | Field[];
127
145
  indexes?: Index[];
128
146
  /** 外键,引用其他表的字段 */
129
147
  foreignKeys?: Record<string, ForeignKey>;
148
+ /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
149
+ phrase?: DictionaryEntry;
130
150
  fields: Record<string, Field>;
131
151
  }
132
152
 
133
153
  // Field builders: type and jsType are fixed, pass extra properties only.
134
- // The field name is written back from the map key later (see buildTable).
154
+ // The field name is written back from the map key later (see defineTable).
135
155
 
136
156
  type FieldExtras<T extends Field> = Omit<T, 'name' | 'type' | 'jsType'>;
137
157
 
@@ -176,33 +196,55 @@ export function jsonField(extra: FieldExtras<JsonField> = {}): JsonField {
176
196
  }
177
197
 
178
198
  export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
179
- const jsType = extra.valueType === 'integer' ? 'number' : 'string';
199
+ const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
180
200
  return { name: '', type: 'enum', jsType, ...extra };
181
201
  }
182
202
 
183
- export function buildTable(
203
+ export function defineTable(
184
204
  name: string,
185
205
  schema: {
186
206
  description?: string;
187
207
  paginated?: boolean;
188
208
  actor?: boolean;
189
209
  generator?: string;
210
+ autoIncrement?: Field;
190
211
  primaryKey?: Field | Field[];
191
212
  indexes?: Index[];
192
213
  foreignKeys?: Record<string, ForeignKey>;
214
+ /** 引用的实体短语(词典条目) */
215
+ phrase?: DictionaryEntry;
193
216
  fields: Record<string, Field>;
194
217
  },
195
218
  ): TableSchema {
196
219
  const table: TableSchema = { name, ...schema };
220
+ for (const key of Object.keys(table.fields)) {
221
+ const field = table.fields[key];
222
+ if (field.schema && field.schema !== table) {
223
+ throw new Error(
224
+ `field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
225
+ );
226
+ }
227
+ }
197
228
  for (const key of Object.keys(table.fields)) {
198
229
  table.fields[key].name = key;
199
230
  table.fields[key].schema = table;
200
231
  }
201
232
  for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
202
233
  const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
203
- for (const ref of refs) {
234
+ const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
235
+ for (let i = 0; i < refs.length; i++) {
236
+ const ref = refs[i];
204
237
  if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
205
238
  if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
239
+ const phrase = (ref.schema as TableSchema).phrase;
240
+ if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
241
+ const expected = `${phrase.name}_${ref.name}`;
242
+ const fkField = fields[i];
243
+ if (fkField.name !== expected) {
244
+ throw new Error(
245
+ `foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
246
+ );
247
+ }
206
248
  }
207
249
  }
208
250
  return table;