@pylonts/dsl 1.0.6 → 1.1.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 (108) hide show
  1. package/README.md +2 -1
  2. package/dist/action.d.ts +7 -0
  3. package/dist/action.js +3 -0
  4. package/dist/asset.d.ts +48 -0
  5. package/dist/asset.js +31 -0
  6. package/dist/bases.d.ts +6 -2
  7. package/dist/bases.js +10 -6
  8. package/dist/check-inheritance.js +1 -4
  9. package/dist/component.d.ts +20 -0
  10. package/dist/component.js +1 -0
  11. package/dist/convert.d.ts +9 -0
  12. package/dist/convert.js +3 -0
  13. package/dist/curd.d.ts +62 -0
  14. package/dist/curd.js +31 -0
  15. package/dist/db-config.d.ts +8 -0
  16. package/dist/db-config.js +1 -0
  17. package/dist/db.d.ts +56 -0
  18. package/dist/db.js +100 -0
  19. package/dist/dictionary.d.ts +26 -5
  20. package/dist/dictionary.js +21 -8
  21. package/dist/dsl.d.ts +10 -49
  22. package/dist/dsl.js +12 -103
  23. package/dist/dto.d.ts +12 -9
  24. package/dist/dto.js +25 -34
  25. package/dist/enum-driver.d.ts +1 -1
  26. package/dist/enum-driver.js +1 -4
  27. package/dist/event.d.ts +8 -0
  28. package/dist/event.js +3 -0
  29. package/dist/flow.d.ts +1 -1
  30. package/dist/flow.js +3 -8
  31. package/dist/import-base.d.ts +15 -0
  32. package/dist/import-base.js +1 -0
  33. package/dist/index.d.ts +31 -17
  34. package/dist/index.js +31 -33
  35. package/dist/mermaid-driver.d.ts +2 -2
  36. package/dist/mermaid-driver.js +4 -7
  37. package/dist/mock.d.ts +12 -0
  38. package/dist/mock.js +1 -0
  39. package/dist/mysql-driver.d.ts +5 -1
  40. package/dist/mysql-driver.js +25 -10
  41. package/dist/navigation.d.ts +22 -0
  42. package/dist/navigation.js +15 -0
  43. package/dist/page-def.d.ts +40 -0
  44. package/dist/page-def.js +38 -0
  45. package/dist/page-flow.d.ts +5 -3
  46. package/dist/page-flow.js +109 -18
  47. package/dist/page.d.ts +37 -15
  48. package/dist/page.js +23 -13
  49. package/dist/pattern.js +2 -6
  50. package/dist/patterns/retry.d.ts +1 -1
  51. package/dist/patterns/retry.js +2 -6
  52. package/dist/popup.d.ts +18 -0
  53. package/dist/popup.js +8 -0
  54. package/dist/project.d.ts +25 -13
  55. package/dist/project.js +41 -6
  56. package/dist/prototype.d.ts +1 -1
  57. package/dist/prototype.js +1 -4
  58. package/dist/provider.d.ts +54 -0
  59. package/dist/provider.js +18 -0
  60. package/dist/ref.d.ts +14 -0
  61. package/dist/ref.js +6 -0
  62. package/dist/route.d.ts +8 -0
  63. package/dist/route.js +3 -0
  64. package/dist/typebox-driver.d.ts +3 -3
  65. package/dist/typebox-driver.js +24 -26
  66. package/dist/utils.d.ts +2 -0
  67. package/dist/utils.js +5 -4
  68. package/docs/curd.md +111 -0
  69. package/docs/dictionary.md +42 -31
  70. package/docs/driver.md +42 -42
  71. package/docs/dto.md +6 -6
  72. package/docs/enum.md +24 -24
  73. package/docs/project.md +2 -2
  74. package/docs/table.md +53 -16
  75. package/package.json +5 -3
  76. package/src/action.ts +11 -0
  77. package/src/asset.ts +63 -0
  78. package/src/bases.ts +30 -20
  79. package/src/component.ts +22 -0
  80. package/src/convert.ts +13 -0
  81. package/src/curd.ts +94 -0
  82. package/src/db-config.ts +8 -0
  83. package/src/db.ts +153 -0
  84. package/src/dictionary.ts +45 -19
  85. package/src/dsl.ts +13 -106
  86. package/src/dto.ts +25 -12
  87. package/src/enum-driver.ts +42 -42
  88. package/src/event.ts +12 -0
  89. package/src/flow.ts +103 -103
  90. package/src/import-base.ts +15 -0
  91. package/src/index.ts +31 -17
  92. package/src/mermaid-driver.ts +5 -4
  93. package/src/mock.ts +12 -0
  94. package/src/mysql-driver.ts +25 -6
  95. package/src/navigation.ts +29 -0
  96. package/src/page-def.ts +80 -0
  97. package/src/page-flow.ts +117 -15
  98. package/src/page.ts +57 -20
  99. package/src/patterns/retry.ts +54 -54
  100. package/src/popup.ts +25 -0
  101. package/src/project.ts +57 -14
  102. package/src/prototype.ts +29 -29
  103. package/src/provider.ts +73 -0
  104. package/src/ref.ts +19 -0
  105. package/src/route.ts +12 -0
  106. package/src/typebox-driver.ts +192 -187
  107. package/src/utils.ts +11 -6
  108. package/src/check-inheritance.ts +0 -86
package/docs/table.md CHANGED
@@ -16,7 +16,9 @@
16
16
  | `enumField` | enum | string / number | VARCHAR(20) / TINYINT | 引用共享枚举定义,见 [enum.md](./enum.md) |
17
17
  | `jsonField` | json | object | JSON | |
18
18
 
19
- 通用扩展属性(构建器第二参数):`label`(中文标签)、`description`、`optional`、`readOnly`、`default`。
19
+ 通用扩展属性(构建器参数):`label`(中文标签)、`description`、`optional`、`readOnly`、`default`。
20
+
21
+ **`optional` 默认语义(MySQL 惯例)**:不写 `optional` 或写 `optional: true` → 列可空,DDL 不渲染 `NOT NULL`;写 `optional: false` → 列必填(`NOT NULL`)。业务上必填的列必须显式声明。
20
22
 
21
23
  ## 定义表
22
24
 
@@ -27,25 +29,50 @@ const id = bigintField({ readOnly: true, label: '主键' });
27
29
 
28
30
  export const order = defineTable('order', {
29
31
  description: '订单',
30
- generator: 'auto_increment',
31
- fields: {
32
+ autoIncrement: id,
33
+ columns: {
32
34
  id,
33
35
  order_no: stringField({ label: '订单号', maxLength: 32, optional: false }),
34
- amount: decimalField({ precision: 18, scale: 2, label: '金额' }),
36
+ amount: decimalField({ precision: 18, scale: 2, label: '金额', optional: false }),
35
37
  },
36
38
  primaryKey: id,
37
39
  });
38
40
  ```
39
41
 
40
- - 字段名从 map key 反写,`fields` 里的 key 就是列名。
42
+ - 字段名从 map key 反写,`columns` 里的 key 就是列名。
41
43
  - 字段实例不可跨表复用(复用同一字段实例会抛错),枚举除外。
42
44
 
45
+ ## 主键生成策略
46
+
47
+ `autoIncrement` 与 `generator` 互斥,二者选一:
48
+
49
+ | 属性 | 含义 | 例子 |
50
+ |---|---|---|
51
+ | `autoIncrement` | 引用自增主键字段,数据库负责生成值(MySQL `AUTO_INCREMENT`)。设了该属性的字段在 DTO 中自动标记为 optional(写入时不需要传) | `autoIncrement: id` |
52
+ | `generator` | 主键由业务侧生成(非数据库自增),告诉下游工具用哪个 ID 生成器 | `generator: 'snowflake'` |
53
+
54
+ ```ts
55
+ // 数据库自增主键
56
+ export const t1 = defineTable('t1', {
57
+ autoIncrement: id,
58
+ columns: { id: bigintField({ readOnly: true, label: '主键' }) },
59
+ primaryKey: id,
60
+ });
61
+
62
+ // 业务生成主键(snowflake)
63
+ export const t2 = defineTable('t2', {
64
+ generator: 'snowflake',
65
+ columns: { id: bigintField({ readOnly: true, label: '主键' }) },
66
+ primaryKey: id,
67
+ });
68
+ ```
69
+
43
70
  ## 索引
44
71
 
45
72
  ```ts
46
73
  indexes: [
47
- { name: 'uk_uuid', fields: c_uuid, unique: true },
48
- { fields: [c_enum, c_date] }, // 名字缺省时 = 字段名 join '_'
74
+ { name: 'uk_uuid', columns: c_uuid, unique: true },
75
+ { columns: [c_enum, c_date] }, // 名字缺省时 = 字段名 join '_'
49
76
  ],
50
77
  ```
51
78
 
@@ -55,44 +82,54 @@ indexes: [
55
82
 
56
83
  ```ts
57
84
  // schema/_dictionary.ts
58
- import { definePhrase } from '@pylonts/dsl';
85
+ import { defineEntityPhrase } from '@pylonts/dsl';
59
86
 
60
- const bd = definePhrase({ name: 'bd', label: 'BD推广员', description: '线下拓展商户的推广人员' });
87
+ export const bd = defineEntityPhrase({ name: 'bd', label: 'BD推广员', description: '线下拓展商户的推广人员' });
61
88
  ```
62
89
 
63
90
  ```ts
64
- import { bd } from './_dictionary';
91
+ // schema/bd.table.ts
92
+ import { bigintField, defineTable } from '@pylonts/dsl';
93
+ import { bd as bdPhrase } from './_dictionary';
65
94
 
66
95
  const bdId = bigintField({ readOnly: true, label: 'BD ID' });
67
96
 
68
97
  export const bd = defineTable('bd', {
69
98
  description: 'BD',
70
- phrase: bd, // 链接词典条目:本表归属的实体
71
- fields: { id: bdId },
99
+ phrase: bdPhrase, // 链接词典条目:本表归属的实体
100
+ columns: { id: bdId },
72
101
  primaryKey: bdId,
73
102
  });
103
+ ```
104
+
105
+ ```ts
106
+ // schema/audit.table.ts
107
+ import { bigintField, defineTable } from '@pylonts/dsl';
108
+ import { bd } from './bd.table';
74
109
 
75
110
  const auditBdId = bigintField({ label: 'BD' });
76
111
 
77
112
  export const audit = defineTable('audit', {
78
113
  description: '审核',
79
- fields: {
114
+ columns: {
80
115
  bd_id: auditBdId, // 列名必须 = 短语 + '_' + 被引用字段名
81
116
  },
82
117
  foreignKeys: {
83
- bd_bd_id: { fields: auditBdId, references: bdId },
118
+ fk_audit_bd: { columns: auditBdId, references: bd.columns.id },
84
119
  },
85
120
  });
86
121
  ```
87
122
 
88
123
  **规则(defineTable 时强制检查)**:外键字段名必须等于 `被引用表.phrase.name + "_" + 被引用字段名`。即引用 `bd.id` 的字段必须叫 `bd_id`——`bd` 来自词典(权威短语),`id` 是 `bd` 表主键。
89
124
 
90
- **短语口径**:`definePhrase` 解释的**就是短语本身**——`name` 即短语词干(如 `mer`),不是实体全名。引用 `merchant` 实体的字段用短语 `mer`(`mer_id`),**不用长语**(`merchant_id`)。短语要短(mer / bd / amt 三字母左右),语义由 `label`/`description` 解释。
125
+ **短语口径**:`defineEntityPhrase`(实体短语)解释的**就是短语本身**——`name` 即短语词干(如 `mer`),不是实体全名。引用 `merchant` 实体的字段用短语 `mer`(`mer_id`),**不用长语**(`merchant_id`)。短语要短(mer / bd / amt 三字母左右),语义由 `label`/`description` 解释。`TableSchema.phrase` 只接受实体短语(`defineEntityPhrase` 产物);业务短语(`defineBusinessPhrase`)用于字段命名后缀校验,见 [field-check.md](../../lint/docs/field-check.md)。
91
126
 
92
127
  - 被引用表未定义 `phrase` → 抛错(检查链要求每个被引用表都有短语)。
93
128
  - 命名不匹配 → 抛错并提示期望名,例如:
94
129
  `foreign key bad: field must be named bd_id (phrase bd + id), got merchant_id`
95
- - 关联表等涉及多个实体的场景不需要 `phrase`,也不建外键。
130
+ - 关联表不需要 `phrase`。
131
+
132
+ > **外键是逻辑作用**:`foreignKeys` 用于定义期命名强校验与关系表达,**DDL 默认不渲染物理 FOREIGN KEY 约束**(`pylonts gen sql init` 不传 `generateForeignKeys`)。数据完整性由 Service/DAO 层保证;如需物理约束,调用 `buildCreateTableSql(schema, { generateForeignKeys: true })`。
96
133
 
97
134
  ## 生成 SQL
98
135
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.0.6",
3
+ "version": "1.1.2",
4
4
  "description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
5
- "type": "commonjs",
5
+ "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
@@ -13,6 +13,7 @@
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.build.json",
15
15
  "prepublishOnly": "npm run build",
16
+ "test": "vitest run",
16
17
  "typecheck": "npx tsc --noEmit"
17
18
  },
18
19
  "keywords": [
@@ -24,6 +25,7 @@
24
25
  "author": "",
25
26
  "license": "MIT",
26
27
  "devDependencies": {
27
- "typescript": "^7.0.2"
28
+ "typescript": "^7.0.2",
29
+ "vitest": "^4.1.10"
28
30
  }
29
31
  }
package/src/action.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { SchemaBase } from './dsl.js';
2
+
3
+ /** An action a user can perform on a page (e.g. submit, approve, reject).
4
+ * Subclasses use `type` as the discriminator. */
5
+ export interface ActionSchema extends SchemaBase {
6
+ type: string;
7
+ }
8
+
9
+ export function defineAction(name: string, description?: string): ActionSchema {
10
+ return { name, description, type: 'gesture' };
11
+ }
package/src/asset.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * defineAsset — registry for reusable project assets (utils, components, flows, pages, hooks).
3
+ *
4
+ * Each asset declares its name, category, import path, tags, and optional usage example.
5
+ * CLI scans all asset declarations, supports query (by tag/category) and generate (import links).
6
+ *
7
+ * // assets/utils.assets.ts
8
+ * import { defineAsset } from '@pylonts/dsl';
9
+ * export const formatAmt = defineAsset({
10
+ * name: 'formatAmt',
11
+ * category: 'util',
12
+ * tags: ['amount', 'format'],
13
+ * import: { name: 'formatAmt', from: '@/utils/amount' },
14
+ * example: 'formatAmt(12345) => "12,345.00"',
15
+ * });
16
+ *
17
+ * // CLI:
18
+ * // pylonts gen asset list --tag form → all form-related assets
19
+ * // pylonts gen asset import formatAmt → import { formatAmt } from '@/utils/amount';
20
+ */
21
+
22
+ export type AssetCategory = 'util' | 'component' | 'flow' | 'page' | 'hook';
23
+
24
+ export interface AssetImport {
25
+ /** Named export, e.g. 'formatAmt' */
26
+ name: string;
27
+ /** Module path, e.g. '@/utils/amount' */
28
+ from: string;
29
+ }
30
+
31
+ export interface AssetConfig {
32
+ name: string;
33
+ category: AssetCategory;
34
+ tags: string[];
35
+ import: AssetImport;
36
+ description?: string;
37
+ /** One-liner usage example */
38
+ example?: string;
39
+ /** Link to detailed docs */
40
+ see?: string;
41
+ }
42
+
43
+ export interface AssetDef {
44
+ name: string;
45
+ category: AssetCategory;
46
+ tags: string[];
47
+ import: AssetImport;
48
+ description?: string;
49
+ example?: string;
50
+ see?: string;
51
+ }
52
+
53
+ export function defineAsset(config: AssetConfig): AssetDef {
54
+ return {
55
+ name: config.name,
56
+ category: config.category,
57
+ tags: config.tags,
58
+ import: config.import,
59
+ description: config.description,
60
+ example: config.example,
61
+ see: config.see,
62
+ };
63
+ }
package/src/bases.ts CHANGED
@@ -1,20 +1,30 @@
1
- import type { DtoMessage, ImportRef } from './dto';
2
-
3
- // Named base-schema references for common protocol DTOs.
4
- //
5
- // These are ImportRef metadata (not re-exports of the actual TypeBox schemas):
6
- // the DSL stores { from, name } so the generator can emit the import line and
7
- // the identifier — the runtime schema object itself is never loaded by the DSL.
8
- //
9
- // `import { PageRequest } from '@pylonts/dsl'` therefore gives .include() an
10
- // already-resolved reference — no static analysis or name lookup needed.
11
-
12
- /** Paginated query request base — renders `import { PageRequest } from '@pylonts/core'` + Intersect */
13
- export const PageRequest: ImportRef = { from: '@pylonts/core', name: 'PageRequest' };
14
-
15
- /** Paginated list response base — renders `import { PageResult } from '@pylonts/core'` + `PageResult(<row>)` */
16
- export const PageResult = (row: DtoMessage): ImportRef => ({
17
- from: '@pylonts/core',
18
- name: 'PageResult',
19
- args: [row],
20
- });
1
+ import type { DtoMessage, ImportBase, ImportRef } from './dto.js';
2
+
3
+ // Named base-schema references for common protocol DTOs.
4
+ //
5
+ // These are ImportRef metadata (not re-exports of the actual TypeBox schemas):
6
+ // the DSL stores { from, name } so the generator can emit the import line and
7
+ // the identifier — the runtime schema object itself is never loaded by the DSL.
8
+ //
9
+ // `import { PageRequest } from '@pylonts/dsl'` therefore gives .include() an
10
+ // already-resolved reference — no static analysis or name lookup needed.
11
+
12
+ /** Paginated query request base — renders `import { PageRequest } from '@pylonts/core'` + Intersect */
13
+ export const PageRequest: ImportBase = { from: '@pylonts/core', name: 'PageRequest' };
14
+
15
+ /** Paginated list response base — renders `import { PageResult } from '@pylonts/core'` + `PageResult(<row>)` */
16
+ export const PageResult = (row: DtoMessage): ImportRef => ({
17
+ from: '@pylonts/core',
18
+ name: 'PageResult',
19
+ args: [row],
20
+ });
21
+
22
+ /** Paged rows type base without generic args — renders `import { PagedRows } from '@pylonts/core'` */
23
+ export const PagedRows: ImportBase = { from: '@pylonts/core', name: 'PagedRows' };
24
+
25
+ /** Paged rows type base — renders `import { PagedRows } from '@pylonts/core'` + `PagedRows(<row>)` */
26
+ export const PageRows = (row: DtoMessage): ImportRef => ({
27
+ from: '@pylonts/core',
28
+ name: 'PagedRows',
29
+ args: [row],
30
+ });
@@ -0,0 +1,22 @@
1
+ import type { SchemaBase } from './dsl.js';
2
+ import type { RefSchema } from './ref.js';
3
+ import type { ActionSchema } from './action.js';
4
+ import type { EventDataSchema } from './event.js';
5
+
6
+ /** A component event trigger declaration. */
7
+ export interface TriggerSchema extends SchemaBase {
8
+ /** Data the event carries (e.g. e.detail). */
9
+ eventData?: EventDataSchema;
10
+ /** Actions that fire when the event occurs. */
11
+ actions?: ActionSchema[];
12
+ }
13
+
14
+ /** A UI component declaration — a virtual schema that describes props and
15
+ * event triggers, not a real renderable component.
16
+ *
17
+ * properties: data bindings via RefSchema (or literal values).
18
+ * triggers: event name → TriggerSchema bindings. */
19
+ export interface ComponentSchema extends SchemaBase {
20
+ properties: Record<string, RefSchema | string | number | boolean>;
21
+ triggers: Record<string, TriggerSchema>;
22
+ }
package/src/convert.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { SchemaBase } from './dsl.js';
2
+
3
+ /** Declares post-call result → page data field mapping.
4
+ * Driver generates per-item transform (e.g. .map()) before setData. */
5
+ export interface ConvertSchema extends SchemaBase {
6
+ type: 'convert';
7
+ /** { targetField: sourceField } — renames or copies fields from call result. */
8
+ fields: Record<string, string>;
9
+ }
10
+
11
+ export function defineConvert(name: string, fields: Record<string, string>): ConvertSchema {
12
+ return { name, type: 'convert', fields };
13
+ }
package/src/curd.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { SchemaBase, Field, Operator } from './dsl.js';
2
+ import { TableSchema } from './db.js';
3
+ import { FrontAppSchema } from './project.js';
4
+ import { ActionSchema } from './action.js';
5
+
6
+ // Admin-only CRUD page standard: binds one entity table to a frontend admin
7
+ // app, describing everything needed to generate the list page plus optional
8
+ // add/update/detail pages. Field-level columns are plain Field instances
9
+ // (table fields, cross-table refs allowed) — DTOs are derived by the generator,
10
+ // the schema itself never references DtoMessage.
11
+
12
+ /** Mode of an action page: modal dialog or standalone route. */
13
+ export type ActionPageMode = 'modal' | 'route';
14
+
15
+ /** One CRUD action page (add / update / detail). */
16
+ export interface ActionPage {
17
+ mode: ActionPageMode;
18
+ /** For add/update: when true, render as modal on list page; when false/undefined, render as standalone route page. */
19
+ modal?: boolean;
20
+ /** Fields rendered on this page. Required, non-empty — every field the
21
+ * frontend shows must be listed explicitly. */
22
+ columns: Field[];
23
+ }
24
+
25
+ /** List page configuration. */
26
+ export interface CurdListConfig {
27
+ /** List columns; required, non-empty. Every field the list shows must be
28
+ * listed explicitly. May include cross-table fields via foreign refs. */
29
+ columns: Field[];
30
+ /** Fuzzy keyword search on this table's columns. */
31
+ keyword?: { columns: Field[] };
32
+ /** Default sort. Required — column and direction are both mandatory. */
33
+ orderBy: { column: Field; direction: 'asc' | 'desc' };
34
+ /** Search condition fields; op defaults to 'eq'. */
35
+ searchFields?: { field: Field; op?: Operator }[];
36
+ /** Column header text overrides: Field.name → header text. */
37
+ columnTitles?: Record<string, string>;
38
+ }
39
+
40
+ /** Admin-only CRUD page standard: binds one entity table to a frontend
41
+ * admin app. Drives generation of the list page plus optional
42
+ * add/update/detail pages. */
43
+ export interface CurdSchema extends SchemaBase {
44
+ /** The admin frontend app this CRUD belongs to (shared instance, type must be 'admin'). */
45
+ app: FrontAppSchema;
46
+ /** The bound entity table (shared instance). */
47
+ table: TableSchema;
48
+ /** List page Chinese title. */
49
+ title: string;
50
+ /** Sidebar menu section (group) this CRUD page belongs to. */
51
+ section: string;
52
+ /** Extra user actions on this page (beyond the standard CRUD). */
53
+ actions?: ActionSchema[];
54
+ /** Add/update/detail action pages. */
55
+ actionPages?: {
56
+ add?: ActionPage;
57
+ update?: ActionPage;
58
+ detail?: ActionPage;
59
+ };
60
+ list: CurdListConfig;
61
+ }
62
+
63
+ function assertColumns(curd: CurdSchema, pageName: string, columns: Field[]): void {
64
+ if (columns.length === 0) {
65
+ throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`);
66
+ }
67
+ }
68
+
69
+ function assertFieldsOwnTable(curd: CurdSchema, label: string, fields: Field[]): void {
70
+ for (const f of fields) {
71
+ if (f.schema !== curd.table) {
72
+ throw new Error(`curd ${curd.name}: ${label} field ${f.name} does not belong to table ${curd.table.name}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ /** Defines an admin CRUD page standard. Runtime-validates admin app binding,
78
+ * non-empty columns and table field ownership (same style as defineTable). */
79
+ export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): CurdSchema {
80
+ const curd: CurdSchema = { name, ...schema };
81
+ if (curd.app.type !== 'admin') {
82
+ throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
83
+ }
84
+ if (!curd.section) {
85
+ throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
86
+ }
87
+ assertColumns(curd, 'list', curd.list.columns);
88
+ for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
89
+ if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
90
+ }
91
+ assertFieldsOwnTable(curd, 'keyword', curd.list.keyword?.columns ?? []);
92
+ assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
93
+ return curd;
94
+ }
@@ -0,0 +1,8 @@
1
+ /** MySQL connection settings, loaded from mysql.config.ts at the project root. */
2
+ export interface DbConfig {
3
+ host: string;
4
+ port: number;
5
+ user: string;
6
+ password: string;
7
+ database: string;
8
+ }
package/src/db.ts ADDED
@@ -0,0 +1,153 @@
1
+ // DB table definitions.
2
+ // Shape: { type: 'table', <extension fields> }
3
+
4
+ import type { EntityPhrase } from './dictionary.js';
5
+ import type { CollectionSchemaBase, EnumDef, Field } from './dsl.js';
6
+
7
+ export type Index = {
8
+ name?: string;
9
+ columns: Field | Field[];
10
+ unique?: boolean;
11
+ };
12
+
13
+ export type ForeignKey = {
14
+ columns: Field | Field[];
15
+ references: Field | Field[];
16
+ };
17
+
18
+ export interface TableSchemaOptions<
19
+ N extends string = string,
20
+ C extends Record<string, Field> = Record<string, Field>,
21
+ E extends Record<string, EnumDef> = Record<string, EnumDef>
22
+ > {
23
+ description?: string;
24
+ paginated?: boolean;
25
+ actor?: boolean;
26
+ generator?: string;
27
+ autoIncrement?: Field;
28
+ /** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
29
+ label?: Field;
30
+ primaryKey?: Field | Field[];
31
+ indexes?: Index[];
32
+ foreignKeys?: Record<string, ForeignKey>;
33
+ /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
34
+ phrase?: EntityPhrase;
35
+ /** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
36
+ enums: E;
37
+ columns: C;
38
+ }
39
+
40
+ export class TableSchema<
41
+ N extends string = string,
42
+ C extends Record<string, Field> = Record<string, Field>,
43
+ E extends Record<string, EnumDef> = Record<string, EnumDef>
44
+ > implements CollectionSchemaBase {
45
+ type = 'table';
46
+ name: N;
47
+ description?: string;
48
+ /** 分页 */
49
+ paginated?: boolean;
50
+ /** 系统操作者(如小程序为 C 端用户,管理端为运营) */
51
+ actor?: boolean;
52
+ /** id 生成器 */
53
+ generator?: string;
54
+ /** 自增主键字段 */
55
+ autoIncrement?: Field;
56
+ primaryKey?: Field | Field[];
57
+ indexes?: Index[];
58
+ /** 外键,引用其他表的字段 */
59
+ foreignKeys?: Record<string, ForeignKey>;
60
+ /** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
61
+ label?: Field;
62
+ /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
63
+ phrase?: EntityPhrase;
64
+ /** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
65
+ enums: E;
66
+ columns: C;
67
+
68
+ constructor(name: N, options: TableSchemaOptions<N, C, E>) {
69
+ if (!options.enums) {
70
+ throw new Error(`table '${name}': enums is required — declare enums: {} when the table has no enums`);
71
+ }
72
+ this.name = name;
73
+ this.description = options.description;
74
+ this.paginated = options.paginated;
75
+ this.actor = options.actor;
76
+ this.generator = options.generator;
77
+ this.autoIncrement = options.autoIncrement;
78
+ this.primaryKey = options.primaryKey;
79
+ this.indexes = options.indexes;
80
+ this.foreignKeys = options.foreignKeys;
81
+ this.label = options.label;
82
+ this.phrase = options.phrase;
83
+ this.enums = options.enums;
84
+ this.columns = options.columns;
85
+ }
86
+
87
+ /** True when the field is part of this table's primary key */
88
+ isPk(fieldRef: Field): boolean {
89
+ if (this.primaryKey === undefined) return false;
90
+ return Array.isArray(this.primaryKey)
91
+ ? this.primaryKey.includes(fieldRef)
92
+ : this.primaryKey === fieldRef;
93
+ }
94
+ }
95
+
96
+ export function defineTable<
97
+ N extends string,
98
+ C extends Record<string, Field>,
99
+ E extends Record<string, EnumDef> = Record<string, EnumDef>
100
+ >(
101
+ name: N,
102
+ schema: TableSchemaOptions<N, C, E>,
103
+ ): TableSchema<N, C, E> {
104
+ const table = new TableSchema<N, C, E>(name, schema);
105
+ if (table.generator && table.autoIncrement) {
106
+ throw new Error(`table '${name}': generator and autoIncrement are mutually exclusive`);
107
+ }
108
+ if (table.label && !Object.values(table.columns).includes(table.label)) {
109
+ throw new Error(`table '${name}': label field '${table.label.name}' must be one of the table's columns`);
110
+ }
111
+ for (const key of Object.keys(table.columns)) {
112
+ const field = table.columns[key] as Field;
113
+ if (field.schema && field.schema !== table) {
114
+ throw new Error(
115
+ `field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
116
+ );
117
+ }
118
+ }
119
+ for (const key of Object.keys(table.columns)) {
120
+ table.columns[key].name = key;
121
+ table.columns[key].schema = table;
122
+ }
123
+ for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
124
+ const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
125
+ const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
126
+ for (let i = 0; i < refs.length; i++) {
127
+ const ref = refs[i];
128
+ if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
129
+ if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
130
+ const phrase = (ref.schema as TableSchema).phrase;
131
+ if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
132
+ const expected = `${phrase.name}_${ref.name}`;
133
+ const fkField = fields[i];
134
+ if (fkField.name !== expected) {
135
+ throw new Error(
136
+ `foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
137
+ );
138
+ }
139
+ }
140
+ }
141
+ for (const [idxKey, index] of Object.entries(table.indexes ?? {})) {
142
+ const cols = Array.isArray(index.columns) ? index.columns : [index.columns];
143
+ for (const col of cols) {
144
+ if (typeof col !== 'object' || col === null || typeof (col as Field).type !== 'string') {
145
+ const idxName = index.name ?? idxKey;
146
+ throw new Error(
147
+ `index ${idxName}: columns must be Field instances (stringField()/intField()/...), got ${JSON.stringify(col)}`,
148
+ );
149
+ }
150
+ }
151
+ }
152
+ return table;
153
+ }
package/src/dictionary.ts CHANGED
@@ -1,20 +1,46 @@
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
- /**
13
- * Explains a phrase — the returned entry IS the phrase. `name` is the phrase
14
- * itself (column-name stem, e.g. mer / bd / amt), not a full entity name;
15
- * label/description explain what it means. Convention: short phrase, not long
16
- * name.
17
- */
18
- export function definePhrase(extra: DictionaryEntry): DictionaryEntry {
19
- return extra;
1
+ import { SchemaBase } from './dsl.js';
2
+
3
+ // Dictionary definitions: vocabulary shared across the team — what a term
4
+ // means and what it is called.
5
+
6
+ /** Kind of a dictionary entry: which naming rule the phrase participates in. */
7
+ export enum DictionaryEntryType {
8
+ Entity = 'entity',
9
+ Business = 'business',
10
+ }
11
+
12
+ /** A vocabulary entry. */
13
+ export interface DictionaryEntry extends SchemaBase {
14
+ /**
15
+ * Entry kind. Entity phrases (abbreviation of an entity, e.g. mer) must be
16
+ * the FIRST segment of a field name; business phrases (attribute of an
17
+ * entity, e.g. rate) must be the LAST segment.
18
+ */
19
+ type: DictionaryEntryType;
20
+ /** Display label (Chinese) for the term. */
21
+ label?: string;
22
+ }
23
+
24
+ /** An entity phrase entry — type narrowed to Entity. */
25
+ export interface EntityPhrase extends DictionaryEntry {
26
+ type: DictionaryEntryType.Entity;
27
+ }
28
+
29
+ /**
30
+ * Explains an entity phrase — the returned entry IS the phrase. `name` is the
31
+ * phrase itself (column-name stem, e.g. mer / bd), not a full entity name;
32
+ * label/description explain what it means. Convention: short phrase, not long
33
+ * name. Entity phrases appear as the first segment of a field name (mer_id).
34
+ */
35
+ export function defineEntityPhrase(extra: Omit<DictionaryEntry, 'type'>): EntityPhrase {
36
+ return { type: DictionaryEntryType.Entity, ...extra };
37
+ }
38
+
39
+ /**
40
+ * Explains a business phrase (an attribute of an entity) — the returned entry
41
+ * IS the phrase. `name` is the phrase itself (column-name stem, e.g. amt /
42
+ * rate). Business phrases appear as the last segment of a field name (bd_rate).
43
+ */
44
+ export function defineBusinessPhrase(extra: Omit<DictionaryEntry, 'type'>): DictionaryEntry {
45
+ return { type: DictionaryEntryType.Business, ...extra };
20
46
  }