@pylonts/dsl 1.0.0 → 1.0.1

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 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,25 @@
1
+ import { SchemaBase } from './dsl';
2
+ /** A vocabulary entry: name is written back from the dictionary key. */
3
+ export interface DictionaryEntry extends SchemaBase {
4
+ }
5
+ export interface DictionarySchema extends SchemaBase {
6
+ entries: Record<string, DictionaryEntry>;
7
+ }
8
+ /** Creates a phrase entry. name is filled in by defineDictionary. */
9
+ export declare function definePhrase(extra?: Omit<DictionaryEntry, 'name'>): DictionaryEntry;
10
+ /** Defines the entity phrase dictionary: standard words aligned with entities. */
11
+ export declare function defineEntityDictionary(schema: {
12
+ description?: string;
13
+ entries: Record<string, DictionaryEntry>;
14
+ }): DictionarySchema;
15
+ /**
16
+ * Defines a business phrase package (one file per industry): entries are the
17
+ * top-level map keys, no wrapping container.
18
+ */
19
+ export declare function defineBusinessDictionary(entries: Record<string, DictionaryEntry>): DictionarySchema;
20
+ /**
21
+ * Merges business phrase packages (one file per industry) into a single flat
22
+ * map. Package metadata (name/description) is dropped; duplicate keys across
23
+ * packages throw.
24
+ */
25
+ export declare function mergeBusinessDictionaries(packages: DictionarySchema[]): Record<string, DictionaryEntry>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.definePhrase = definePhrase;
4
+ exports.defineEntityDictionary = defineEntityDictionary;
5
+ exports.defineBusinessDictionary = defineBusinessDictionary;
6
+ exports.mergeBusinessDictionaries = mergeBusinessDictionaries;
7
+ /** Creates a phrase entry. name is filled in by defineDictionary. */
8
+ function definePhrase(extra = {}) {
9
+ return { name: '', ...extra };
10
+ }
11
+ function defineDictionary(schema) {
12
+ const dictionary = { ...schema };
13
+ for (const key of Object.keys(dictionary.entries)) {
14
+ dictionary.entries[key].name = key;
15
+ }
16
+ return dictionary;
17
+ }
18
+ /** Defines the entity phrase dictionary: standard words aligned with entities. */
19
+ function defineEntityDictionary(schema) {
20
+ return defineDictionary({ name: 'entity', ...schema });
21
+ }
22
+ /**
23
+ * Defines a business phrase package (one file per industry): entries are the
24
+ * top-level map keys, no wrapping container.
25
+ */
26
+ function defineBusinessDictionary(entries) {
27
+ return defineDictionary({ name: 'business', entries });
28
+ }
29
+ /**
30
+ * Merges business phrase packages (one file per industry) into a single flat
31
+ * map. Package metadata (name/description) is dropped; duplicate keys across
32
+ * packages throw.
33
+ */
34
+ function mergeBusinessDictionaries(packages) {
35
+ const merged = {};
36
+ for (const pkg of packages) {
37
+ for (const key of Object.keys(pkg.entries)) {
38
+ if (merged[key]) {
39
+ throw new Error(`business phrase ${key} is defined in multiple packages`);
40
+ }
41
+ merged[key] = pkg.entries[key];
42
+ }
43
+ }
44
+ return merged;
45
+ }
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,11 +119,12 @@ 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>;
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/index.d.ts CHANGED
@@ -1,5 +1,8 @@
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';
package/dist/index.js CHANGED
@@ -16,6 +16,9 @@ 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);
@@ -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,30 @@
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
+ }
17
+ export interface ProjectSchema extends SchemaBase {
18
+ apps: FrontApp[];
19
+ apis: ProjectApi[];
20
+ }
21
+ /**
22
+ * Defines the project topology. FrontApp instances are shared value objects:
23
+ * api.apps references the same instances from project.apps, so an app served
24
+ * by multiple APIs is defined once and referenced many times.
25
+ */
26
+ export declare function defineProject(name: string, schema: {
27
+ description?: string;
28
+ apps: FrontApp[];
29
+ apis: ProjectApi[];
30
+ }): 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 };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",
@@ -0,0 +1,63 @@
1
+ import { SchemaBase } from './dsl';
2
+
3
+ // Dictionary definitions: vocabulary shared across the team — what a term
4
+ // means and what it is called. Entries are written back from the map key,
5
+ // the same mechanism as defineTable fields.
6
+
7
+ /** A vocabulary entry: name is written back from the dictionary key. */
8
+ export interface DictionaryEntry extends SchemaBase {}
9
+
10
+ export interface DictionarySchema extends SchemaBase {
11
+ entries: Record<string, DictionaryEntry>;
12
+ }
13
+
14
+ /** Creates a phrase entry. name is filled in by defineDictionary. */
15
+ export function definePhrase(extra: Omit<DictionaryEntry, 'name'> = {}): DictionaryEntry {
16
+ return { name: '', ...extra };
17
+ }
18
+
19
+ function defineDictionary(schema: {
20
+ name: string;
21
+ description?: string;
22
+ entries: Record<string, DictionaryEntry>;
23
+ }): DictionarySchema {
24
+ const dictionary: DictionarySchema = { ...schema };
25
+ for (const key of Object.keys(dictionary.entries)) {
26
+ dictionary.entries[key].name = key;
27
+ }
28
+ return dictionary;
29
+ }
30
+
31
+ /** Defines the entity phrase dictionary: standard words aligned with entities. */
32
+ export function defineEntityDictionary(schema: {
33
+ description?: string;
34
+ entries: Record<string, DictionaryEntry>;
35
+ }): DictionarySchema {
36
+ return defineDictionary({ name: 'entity', ...schema });
37
+ }
38
+
39
+ /**
40
+ * Defines a business phrase package (one file per industry): entries are the
41
+ * top-level map keys, no wrapping container.
42
+ */
43
+ export function defineBusinessDictionary(entries: Record<string, DictionaryEntry>): DictionarySchema {
44
+ return defineDictionary({ name: 'business', entries });
45
+ }
46
+
47
+ /**
48
+ * Merges business phrase packages (one file per industry) into a single flat
49
+ * map. Package metadata (name/description) is dropped; duplicate keys across
50
+ * packages throw.
51
+ */
52
+ export function mergeBusinessDictionaries(packages: DictionarySchema[]): Record<string, DictionaryEntry> {
53
+ const merged: Record<string, DictionaryEntry> = {};
54
+ for (const pkg of packages) {
55
+ for (const key of Object.keys(pkg.entries)) {
56
+ if (merged[key]) {
57
+ throw new Error(`business phrase ${key} is defined in multiple packages`);
58
+ }
59
+ merged[key] = pkg.entries[key];
60
+ }
61
+ }
62
+ return merged;
63
+ }
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,17 +196,18 @@ 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>;
@@ -194,15 +215,34 @@ export function buildTable(
194
215
  },
195
216
  ): TableSchema {
196
217
  const table: TableSchema = { name, ...schema };
218
+ for (const key of Object.keys(table.fields)) {
219
+ const field = table.fields[key];
220
+ if (field.schema && field.schema !== table) {
221
+ throw new Error(
222
+ `field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
223
+ );
224
+ }
225
+ }
197
226
  for (const key of Object.keys(table.fields)) {
198
227
  table.fields[key].name = key;
199
228
  table.fields[key].schema = table;
200
229
  }
201
230
  for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
202
231
  const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
203
- for (const ref of refs) {
232
+ const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
233
+ for (let i = 0; i < refs.length; i++) {
234
+ const ref = refs[i];
204
235
  if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
205
236
  if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
237
+ const phrase = (ref.schema as TableSchema).phrase;
238
+ if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
239
+ const expected = `${phrase.name}_${ref.name}`;
240
+ const fkField = fields[i];
241
+ if (fkField.name !== expected) {
242
+ throw new Error(
243
+ `foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
244
+ );
245
+ }
206
246
  }
207
247
  }
208
248
  return table;
@@ -1,6 +1,6 @@
1
- import { EnumField } from './dsl';
1
+ import { EnumDef } from './dsl';
2
2
 
3
- // Enum driver: renders an EnumField into a standalone TypeScript enum file.
3
+ // Enum driver: renders an EnumDef into a standalone TypeScript enum file.
4
4
  // Shape matches the generated-enum product consumed by the TypeBox driver (Type.Enum):
5
5
  //
6
6
  // export enum UserStatus {
@@ -26,11 +26,10 @@ function labelName(jsName: string): string {
26
26
  return `${jsName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_LABEL`;
27
27
  }
28
28
 
29
- export function renderEnum(field: EnumField): string {
30
- if (!field.jsName) throw new Error(`enum field ${field.name} requires jsName to render`);
31
- const name = field.jsName;
32
- const members = field.values.map((v) => ` ${v.symbol} = ${renderValue(v.value)},`);
33
- const labels = field.values.map((v) => ` [${name}.${v.symbol}]: ${renderString(v.label)},`);
29
+ export function renderEnum(def: EnumDef): string {
30
+ const name = def.jsName;
31
+ const members = def.values.map((v) => ` ${v.symbol} = ${renderValue(v.value)},`);
32
+ const labels = def.values.map((v) => ` [${name}.${v.symbol}]: ${renderString(v.label)},`);
34
33
  return [
35
34
  `export enum ${name} {`,
36
35
  ...members,
@@ -41,4 +40,4 @@ export function renderEnum(field: EnumField): string {
41
40
  '};',
42
41
  '',
43
42
  ].join('\n');
44
- }
43
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
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
- export * from './typebox-driver';
8
+ export * from './typebox-driver';
@@ -1,4 +1,4 @@
1
- import { Field, Index, TableSchema } from './dsl';
1
+ import { Field, ForeignKey, Index, TableSchema } from './dsl';
2
2
 
3
3
  // MySQL driver: converts a TableSchema into a CREATE TABLE statement.
4
4
 
@@ -25,16 +25,17 @@ function columnType(field: Field): string {
25
25
  return 'DATETIME';
26
26
  case 'enum':
27
27
  // Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT.
28
- return field.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
28
+ return field.enum.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
29
29
  case 'json':
30
30
  return 'JSON';
31
31
  }
32
32
  }
33
33
 
34
- function columnDef(field: Field): string {
34
+ function columnDef(field: Field, autoIncrement?: Field): string {
35
35
  const parts = [field.name, columnType(field)];
36
36
  if (field.optional === false) parts.push('NOT NULL');
37
37
  if (field.default !== undefined) parts.push(`DEFAULT '${field.default}'`);
38
+ if (field === autoIncrement) parts.push('AUTO_INCREMENT');
38
39
  return parts.join(' ');
39
40
  }
40
41
 
@@ -51,10 +52,30 @@ function indexClause(index: Index): string {
51
52
  return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
52
53
  }
53
54
 
54
- export function buildCreateTableSql(schema: TableSchema): string {
55
- const lines = Object.values(schema.fields).map(columnDef);
55
+ function foreignKeyClause(name: string, fk: ForeignKey): string {
56
+ const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
57
+ const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
58
+ const refTable = refs[0].schema?.name;
59
+ if (!refTable) throw new Error(`foreign key ${name}: references field has no schema`);
60
+ const fkCols = fields.map((f) => f.name).join(', ');
61
+ const refCols = refs.map((r) => r.name).join(', ');
62
+ return `CONSTRAINT \`${name}\` FOREIGN KEY (${fkCols}) REFERENCES \`${refTable}\` (${refCols})`;
63
+ }
64
+
65
+ export interface BuildCreateTableSqlOptions {
66
+ /** 是否生成外键约束,默认 false(不生成) */
67
+ generateForeignKeys?: boolean;
68
+ }
69
+
70
+ export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTableSqlOptions = {}): string {
71
+ const lines = Object.values(schema.fields).map((field) => columnDef(field, schema.autoIncrement));
56
72
  const pk = primaryKeyClause(schema);
57
73
  if (pk) lines.push(pk);
58
74
  for (const index of schema.indexes ?? []) lines.push(indexClause(index));
75
+ if (options.generateForeignKeys) {
76
+ for (const [name, fk] of Object.entries(schema.foreignKeys ?? {})) {
77
+ lines.push(foreignKeyClause(name, fk));
78
+ }
79
+ }
59
80
  return `CREATE TABLE \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
60
81
  }
package/src/project.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { SchemaBase } from './dsl';
2
+
3
+ // Project topology definitions: describe the applications (frontends) and
4
+ // backend APIs of a repository, and which frontends each API serves.
5
+
6
+ /** Frontend form factor. Closed enum, extend when new form factors appear. */
7
+ export type FrontType = 'admin' | 'wxmini';
8
+
9
+ /** A frontend application (e.g. admin console, wechat mini program). */
10
+ export interface FrontApp extends SchemaBase {
11
+ type: FrontType;
12
+ /** Source directory relative to project root, e.g. 'web-admin/'. */
13
+ dir: string;
14
+ }
15
+
16
+ /** A backend API service. apps references shared FrontApp instances. */
17
+ export interface ProjectApi extends SchemaBase {
18
+ /** Source directory relative to project root, e.g. 'api/'. */
19
+ dir: string;
20
+ /** Frontends this API serves. Direct instance references (see defineProject). */
21
+ apps: FrontApp[];
22
+ }
23
+
24
+ export interface ProjectSchema extends SchemaBase {
25
+ apps: FrontApp[];
26
+ apis: ProjectApi[];
27
+ }
28
+
29
+ /**
30
+ * Defines the project topology. FrontApp instances are shared value objects:
31
+ * api.apps references the same instances from project.apps, so an app served
32
+ * by multiple APIs is defined once and referenced many times.
33
+ */
34
+ export function defineProject(
35
+ name: string,
36
+ schema: {
37
+ description?: string;
38
+ apps: FrontApp[];
39
+ apis: ProjectApi[];
40
+ },
41
+ ): ProjectSchema {
42
+ return { name, ...schema };
43
+ }
@@ -0,0 +1,30 @@
1
+ import { SchemaBase } from './dsl';
2
+
3
+ // Prototype definitions: high-level design of a page. A prototype lists only
4
+ // the fields a page needs — no types, no bindings to apps/APIs/tables. Field
5
+ // details (DTO/table definitions) are written separately and connected later.
6
+
7
+ /** Display metadata for a prototype field. */
8
+ export interface PrototypeFieldMeta {
9
+ label: string;
10
+ description?: string;
11
+ }
12
+
13
+ export interface PrototypeSchema extends SchemaBase {
14
+ /** Field requirements: key is the field name, value is display metadata. */
15
+ fields: Record<string, PrototypeFieldMeta>;
16
+ }
17
+
18
+ /**
19
+ * Defines a page prototype. The field keys become the names referenced by
20
+ * later DTO/table definitions; here they only carry label/description.
21
+ */
22
+ export function definePrototype(
23
+ name: string,
24
+ schema: {
25
+ description?: string;
26
+ fields: Record<string, PrototypeFieldMeta>;
27
+ },
28
+ ): PrototypeSchema {
29
+ return { name, ...schema };
30
+ }
@@ -50,9 +50,8 @@ function renderBasic(field: Field, pattern: string | undefined, resolver: EnumRe
50
50
  case 'json':
51
51
  return 'Type.Unknown()';
52
52
  case 'enum': {
53
- if (!field.jsName) throw new Error(`enum field ${field.name} requires jsName to render`);
54
- const ref = resolver?.(field.jsName);
55
- if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.jsName} — pass an EnumResolver`);
53
+ const ref = resolver?.(field.enum.jsName);
54
+ if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
56
55
  return `Type.Enum(${ref.name})`;
57
56
  }
58
57
  default:
@@ -97,9 +96,8 @@ function collectEnumImports(
97
96
  return;
98
97
  }
99
98
  if (f.field.type === 'enum') {
100
- if (!f.field.jsName) throw new Error(`enum field ${f.field.name} requires jsName to render`);
101
- const ref = resolver?.(f.field.jsName);
102
- if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.jsName} — pass an EnumResolver`);
99
+ const ref = resolver?.(f.field.enum.jsName);
100
+ if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
103
101
  out.set(`${ref.from}#${ref.name}`, ref);
104
102
  }
105
103
  }