@pylonts/dsl 1.0.0
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 +94 -0
- package/dist/dsl.d.ts +121 -0
- package/dist/dsl.js +67 -0
- package/dist/dto.d.ts +81 -0
- package/dist/dto.js +127 -0
- package/dist/enum-driver.d.ts +2 -0
- package/dist/enum-driver.js +42 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/mysql-driver.d.ts +2 -0
- package/dist/mysql-driver.js +62 -0
- package/dist/typebox-driver.d.ts +6 -0
- package/dist/typebox-driver.js +126 -0
- package/package.json +28 -0
- package/src/dsl.ts +209 -0
- package/src/dto.ts +185 -0
- package/src/enum-driver.ts +44 -0
- package/src/index.ts +5 -0
- package/src/mysql-driver.ts +60 -0
- package/src/typebox-driver.ts +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @pylonts/dsl
|
|
2
|
+
|
|
3
|
+
Schema 定义与产物生成分离的 DSL 系统:先写 DSL 元数据(表 / DTO),再由 driver 翻译成目标产物(SQL、TS 枚举、TypeBox 源码)。
|
|
4
|
+
|
|
5
|
+
## 结构
|
|
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 参数校验 |
|
|
27
|
+
|
|
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
|
+
```
|
|
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
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 关键语义
|
|
89
|
+
|
|
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({…})])`。
|
package/dist/dsl.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export interface SchemaBase {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface BaseField {
|
|
6
|
+
name: string;
|
|
7
|
+
/** 显示名称(中文标签) */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** 字段描述 */
|
|
10
|
+
description?: string;
|
|
11
|
+
optional?: boolean;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
default?: string;
|
|
14
|
+
/** 所属 schema(db 或 dto) */
|
|
15
|
+
schema?: SchemaBase;
|
|
16
|
+
}
|
|
17
|
+
interface StringField extends BaseField {
|
|
18
|
+
type: 'string';
|
|
19
|
+
jsType: 'string';
|
|
20
|
+
minLength?: number;
|
|
21
|
+
maxLength?: number;
|
|
22
|
+
}
|
|
23
|
+
interface TextField extends BaseField {
|
|
24
|
+
type: 'text';
|
|
25
|
+
jsType: 'string';
|
|
26
|
+
}
|
|
27
|
+
interface IntField extends BaseField {
|
|
28
|
+
type: 'integer';
|
|
29
|
+
jsType: 'number';
|
|
30
|
+
min?: number;
|
|
31
|
+
max?: number;
|
|
32
|
+
}
|
|
33
|
+
interface BigintField extends BaseField {
|
|
34
|
+
type: 'bigint';
|
|
35
|
+
jsType: 'string';
|
|
36
|
+
}
|
|
37
|
+
interface DecimalField extends BaseField {
|
|
38
|
+
type: 'decimal';
|
|
39
|
+
jsType: 'string';
|
|
40
|
+
precision: number;
|
|
41
|
+
scale: number;
|
|
42
|
+
}
|
|
43
|
+
interface BooleanField extends BaseField {
|
|
44
|
+
type: 'boolean';
|
|
45
|
+
jsType: 'boolean';
|
|
46
|
+
}
|
|
47
|
+
interface DateField extends BaseField {
|
|
48
|
+
type: 'date';
|
|
49
|
+
jsType: 'Date';
|
|
50
|
+
}
|
|
51
|
+
interface TimeField extends BaseField {
|
|
52
|
+
type: 'time';
|
|
53
|
+
jsType: 'string';
|
|
54
|
+
}
|
|
55
|
+
interface DateTimeField extends BaseField {
|
|
56
|
+
type: 'datetime';
|
|
57
|
+
jsType: 'Date';
|
|
58
|
+
}
|
|
59
|
+
export interface EnumValue {
|
|
60
|
+
value: string | number;
|
|
61
|
+
symbol: string;
|
|
62
|
+
label: string;
|
|
63
|
+
}
|
|
64
|
+
export interface EnumField extends BaseField {
|
|
65
|
+
type: 'enum';
|
|
66
|
+
jsType: 'string' | 'number';
|
|
67
|
+
valueType: 'string' | 'integer';
|
|
68
|
+
/** JS 定义名称,如 MerchantStatus */
|
|
69
|
+
jsName?: string;
|
|
70
|
+
values: EnumValue[];
|
|
71
|
+
}
|
|
72
|
+
interface JsonField extends BaseField {
|
|
73
|
+
type: 'json';
|
|
74
|
+
jsType: 'object';
|
|
75
|
+
}
|
|
76
|
+
export type Field = StringField | TextField | IntField | BigintField | DecimalField | BooleanField | DateField | TimeField | DateTimeField | EnumField | JsonField;
|
|
77
|
+
export type Index = {
|
|
78
|
+
name?: string;
|
|
79
|
+
fields: Field | Field[];
|
|
80
|
+
unique?: boolean;
|
|
81
|
+
};
|
|
82
|
+
export type ForeignKey = {
|
|
83
|
+
fields: Field | Field[];
|
|
84
|
+
references: Field | Field[];
|
|
85
|
+
};
|
|
86
|
+
export interface TableSchema extends SchemaBase {
|
|
87
|
+
/** 分页 */
|
|
88
|
+
paginated?: boolean;
|
|
89
|
+
/** 系统操作者(如小程序为 C 端用户,管理端为运营) */
|
|
90
|
+
actor?: boolean;
|
|
91
|
+
/** id 生成器 */
|
|
92
|
+
generator?: string;
|
|
93
|
+
primaryKey?: Field | Field[];
|
|
94
|
+
indexes?: Index[];
|
|
95
|
+
/** 外键,引用其他表的字段 */
|
|
96
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
97
|
+
fields: Record<string, Field>;
|
|
98
|
+
}
|
|
99
|
+
type FieldExtras<T extends Field> = Omit<T, 'name' | 'type' | 'jsType'>;
|
|
100
|
+
export declare function stringField(extra?: FieldExtras<StringField>): StringField;
|
|
101
|
+
export declare function textField(extra?: FieldExtras<TextField>): TextField;
|
|
102
|
+
export declare function intField(extra?: FieldExtras<IntField>): IntField;
|
|
103
|
+
export declare function bigintField(extra?: FieldExtras<BigintField>): BigintField;
|
|
104
|
+
export declare function decimalField(extra: FieldExtras<DecimalField>): DecimalField;
|
|
105
|
+
export declare function booleanField(extra?: FieldExtras<BooleanField>): BooleanField;
|
|
106
|
+
export declare function dateField(extra?: FieldExtras<DateField>): DateField;
|
|
107
|
+
export declare function timeField(extra?: FieldExtras<TimeField>): TimeField;
|
|
108
|
+
export declare function datetimeField(extra?: FieldExtras<DateTimeField>): DateTimeField;
|
|
109
|
+
export declare function jsonField(extra?: FieldExtras<JsonField>): JsonField;
|
|
110
|
+
export declare function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField;
|
|
111
|
+
export declare function buildTable(name: string, schema: {
|
|
112
|
+
description?: string;
|
|
113
|
+
paginated?: boolean;
|
|
114
|
+
actor?: boolean;
|
|
115
|
+
generator?: string;
|
|
116
|
+
primaryKey?: Field | Field[];
|
|
117
|
+
indexes?: Index[];
|
|
118
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
119
|
+
fields: Record<string, Field>;
|
|
120
|
+
}): TableSchema;
|
|
121
|
+
export {};
|
package/dist/dsl.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// DSL field type definitions.
|
|
3
|
+
// Shape: { type: <type name>, <extension fields> }
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.stringField = stringField;
|
|
6
|
+
exports.textField = textField;
|
|
7
|
+
exports.intField = intField;
|
|
8
|
+
exports.bigintField = bigintField;
|
|
9
|
+
exports.decimalField = decimalField;
|
|
10
|
+
exports.booleanField = booleanField;
|
|
11
|
+
exports.dateField = dateField;
|
|
12
|
+
exports.timeField = timeField;
|
|
13
|
+
exports.datetimeField = datetimeField;
|
|
14
|
+
exports.jsonField = jsonField;
|
|
15
|
+
exports.enumField = enumField;
|
|
16
|
+
exports.buildTable = buildTable;
|
|
17
|
+
function stringField(extra = {}) {
|
|
18
|
+
return { name: '', type: 'string', jsType: 'string', ...extra };
|
|
19
|
+
}
|
|
20
|
+
function textField(extra = {}) {
|
|
21
|
+
return { name: '', type: 'text', jsType: 'string', ...extra };
|
|
22
|
+
}
|
|
23
|
+
function intField(extra = {}) {
|
|
24
|
+
return { name: '', type: 'integer', jsType: 'number', ...extra };
|
|
25
|
+
}
|
|
26
|
+
function bigintField(extra = {}) {
|
|
27
|
+
return { name: '', type: 'bigint', jsType: 'string', ...extra };
|
|
28
|
+
}
|
|
29
|
+
function decimalField(extra) {
|
|
30
|
+
return { name: '', type: 'decimal', jsType: 'string', ...extra };
|
|
31
|
+
}
|
|
32
|
+
function booleanField(extra = {}) {
|
|
33
|
+
return { name: '', type: 'boolean', jsType: 'boolean', ...extra };
|
|
34
|
+
}
|
|
35
|
+
function dateField(extra = {}) {
|
|
36
|
+
return { name: '', type: 'date', jsType: 'Date', ...extra };
|
|
37
|
+
}
|
|
38
|
+
function timeField(extra = {}) {
|
|
39
|
+
return { name: '', type: 'time', jsType: 'string', ...extra };
|
|
40
|
+
}
|
|
41
|
+
function datetimeField(extra = {}) {
|
|
42
|
+
return { name: '', type: 'datetime', jsType: 'Date', ...extra };
|
|
43
|
+
}
|
|
44
|
+
function jsonField(extra = {}) {
|
|
45
|
+
return { name: '', type: 'json', jsType: 'object', ...extra };
|
|
46
|
+
}
|
|
47
|
+
function enumField(extra) {
|
|
48
|
+
const jsType = extra.valueType === 'integer' ? 'number' : 'string';
|
|
49
|
+
return { name: '', type: 'enum', jsType, ...extra };
|
|
50
|
+
}
|
|
51
|
+
function buildTable(name, schema) {
|
|
52
|
+
const table = { name, ...schema };
|
|
53
|
+
for (const key of Object.keys(table.fields)) {
|
|
54
|
+
table.fields[key].name = key;
|
|
55
|
+
table.fields[key].schema = table;
|
|
56
|
+
}
|
|
57
|
+
for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
|
|
58
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
59
|
+
for (const ref of refs) {
|
|
60
|
+
if (!ref.schema)
|
|
61
|
+
throw new Error(`foreign key ${fkName}: references field has no schema`);
|
|
62
|
+
if (ref.schema === table)
|
|
63
|
+
throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return table;
|
|
67
|
+
}
|
package/dist/dto.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BaseField, Field, SchemaBase, TableSchema } from './dsl';
|
|
2
|
+
export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne';
|
|
3
|
+
/**
|
|
4
|
+
* Reference to an existing TypeBox base schema by its import location.
|
|
5
|
+
* Serializable metadata: the driver renders `import { name } from 'from'`
|
|
6
|
+
* and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
|
|
7
|
+
*/
|
|
8
|
+
export interface ImportRef {
|
|
9
|
+
/** Module specifier, e.g. '@pylonts/core' */
|
|
10
|
+
from: string;
|
|
11
|
+
/** Named export, e.g. 'PageRequest' */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Generic type arguments for the base schema (same-file DTO export names), e.g. PageResult(AdvertRow) */
|
|
14
|
+
args?: string[];
|
|
15
|
+
}
|
|
16
|
+
export type DtoExtras = {
|
|
17
|
+
pattern?: string;
|
|
18
|
+
/** 联合判断是否可选,定义后优先级高于 field.optional */
|
|
19
|
+
optional?: boolean;
|
|
20
|
+
/** 查询比较操作符(query 方向字段) */
|
|
21
|
+
operator?: Operator;
|
|
22
|
+
};
|
|
23
|
+
export type DtoArrayFieldDef = BaseField & {
|
|
24
|
+
type: 'array';
|
|
25
|
+
jsType: 'array';
|
|
26
|
+
items: DtoField;
|
|
27
|
+
};
|
|
28
|
+
export type DtoObjectFieldDef = BaseField & {
|
|
29
|
+
type: 'object';
|
|
30
|
+
jsType: 'object';
|
|
31
|
+
properties: Record<string, DtoField>;
|
|
32
|
+
};
|
|
33
|
+
export declare class DtoField {
|
|
34
|
+
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
35
|
+
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
36
|
+
name: string;
|
|
37
|
+
/** 所属 DTO 容器(buildMessage 反写) */
|
|
38
|
+
schema?: DtoMessage;
|
|
39
|
+
field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
|
|
40
|
+
pattern?: string;
|
|
41
|
+
optional?: boolean;
|
|
42
|
+
operator?: Operator;
|
|
43
|
+
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef, extra?: DtoExtras);
|
|
44
|
+
setPattern(value: string): this;
|
|
45
|
+
setOptional(value: boolean): this;
|
|
46
|
+
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
47
|
+
op(value: Operator): this;
|
|
48
|
+
/** optional 优先于 field.optional */
|
|
49
|
+
isOptional(): boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare class DtoArrayField extends DtoField {
|
|
52
|
+
field: DtoArrayFieldDef;
|
|
53
|
+
items(): DtoField;
|
|
54
|
+
}
|
|
55
|
+
export declare class DtoObjectField extends DtoField {
|
|
56
|
+
field: DtoObjectFieldDef;
|
|
57
|
+
properties(): Record<string, DtoField>;
|
|
58
|
+
}
|
|
59
|
+
export type DtoDirection = 'input' | 'output' | 'query' | 'pk';
|
|
60
|
+
export interface DtoMessage extends SchemaBase {
|
|
61
|
+
/** 方向:输入或输出 */
|
|
62
|
+
direction: DtoDirection;
|
|
63
|
+
fields: Record<string, DtoField>;
|
|
64
|
+
/** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
|
|
65
|
+
bases?: ImportRef[];
|
|
66
|
+
/** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
|
|
67
|
+
include(...refs: ImportRef[]): DtoMessage;
|
|
68
|
+
}
|
|
69
|
+
export declare function dtoField(field: Field, extra?: DtoExtras): DtoField;
|
|
70
|
+
export declare function dtoArrayField(def: {
|
|
71
|
+
items: DtoField;
|
|
72
|
+
} & Omit<BaseField, 'name'>, extra?: DtoExtras): DtoArrayField;
|
|
73
|
+
export declare function dtoObjectField(def: {
|
|
74
|
+
properties: Record<string, DtoField>;
|
|
75
|
+
} & Omit<BaseField, 'name'>, extra?: DtoExtras): DtoObjectField;
|
|
76
|
+
export declare function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
77
|
+
export declare function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
78
|
+
export declare function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
79
|
+
export declare function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
80
|
+
/** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
|
|
81
|
+
export declare function from(table: TableSchema, fields: Field[]): Record<string, DtoField>;
|
package/dist/dto.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DtoObjectField = exports.DtoArrayField = exports.DtoField = void 0;
|
|
4
|
+
exports.dtoField = dtoField;
|
|
5
|
+
exports.dtoArrayField = dtoArrayField;
|
|
6
|
+
exports.dtoObjectField = dtoObjectField;
|
|
7
|
+
exports.buildInput = buildInput;
|
|
8
|
+
exports.buildOutput = buildOutput;
|
|
9
|
+
exports.buildQuery = buildQuery;
|
|
10
|
+
exports.buildPk = buildPk;
|
|
11
|
+
exports.from = from;
|
|
12
|
+
class DtoField {
|
|
13
|
+
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
14
|
+
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
15
|
+
name;
|
|
16
|
+
/** 所属 DTO 容器(buildMessage 反写) */
|
|
17
|
+
schema;
|
|
18
|
+
field;
|
|
19
|
+
pattern;
|
|
20
|
+
optional;
|
|
21
|
+
operator;
|
|
22
|
+
constructor(field, extra = {}) {
|
|
23
|
+
this.name = '';
|
|
24
|
+
this.field = field;
|
|
25
|
+
this.pattern = extra.pattern;
|
|
26
|
+
this.optional = extra.optional;
|
|
27
|
+
this.operator = extra.operator;
|
|
28
|
+
}
|
|
29
|
+
setPattern(value) {
|
|
30
|
+
this.pattern = value;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
setOptional(value) {
|
|
34
|
+
this.optional = value;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
38
|
+
op(value) {
|
|
39
|
+
this.operator = value;
|
|
40
|
+
this.optional = true;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
/** optional 优先于 field.optional */
|
|
44
|
+
isOptional() {
|
|
45
|
+
if (this.optional !== undefined)
|
|
46
|
+
return this.optional;
|
|
47
|
+
return this.field.optional ?? false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.DtoField = DtoField;
|
|
51
|
+
class DtoArrayField extends DtoField {
|
|
52
|
+
items() {
|
|
53
|
+
return this.field.items;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.DtoArrayField = DtoArrayField;
|
|
57
|
+
class DtoObjectField extends DtoField {
|
|
58
|
+
properties() {
|
|
59
|
+
return this.field.properties;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.DtoObjectField = DtoObjectField;
|
|
63
|
+
function dtoField(field, extra = {}) {
|
|
64
|
+
return new DtoField(field, extra);
|
|
65
|
+
}
|
|
66
|
+
function dtoArrayField(def, extra = {}) {
|
|
67
|
+
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def }, extra);
|
|
68
|
+
}
|
|
69
|
+
function dtoObjectField(def, extra = {}) {
|
|
70
|
+
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def }, extra);
|
|
71
|
+
}
|
|
72
|
+
function buildMessage(name, direction, fields, description) {
|
|
73
|
+
const message = {
|
|
74
|
+
name,
|
|
75
|
+
direction,
|
|
76
|
+
description,
|
|
77
|
+
fields,
|
|
78
|
+
bases: [],
|
|
79
|
+
include(...refs) {
|
|
80
|
+
this.bases.push(...refs);
|
|
81
|
+
return this;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
if (direction === 'query') {
|
|
85
|
+
// Rule B: query/search fields are always optional.
|
|
86
|
+
for (const field of Object.values(message.fields))
|
|
87
|
+
field.optional = true;
|
|
88
|
+
}
|
|
89
|
+
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
90
|
+
// are created per DTO, never shared).
|
|
91
|
+
for (const key of Object.keys(message.fields)) {
|
|
92
|
+
const df = message.fields[key];
|
|
93
|
+
df.name = key;
|
|
94
|
+
df.schema = message;
|
|
95
|
+
// Custom (inline) fields are owned by this DTO: write back name + schema.
|
|
96
|
+
// Fields picked via from() share the database Field instance whose
|
|
97
|
+
// name/schema already point to the table — leave them untouched.
|
|
98
|
+
if (df.field.schema === undefined) {
|
|
99
|
+
df.field.name = key;
|
|
100
|
+
df.field.schema = message;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return message;
|
|
104
|
+
}
|
|
105
|
+
function buildInput(name, fields, description) {
|
|
106
|
+
return buildMessage(name, 'input', fields, description);
|
|
107
|
+
}
|
|
108
|
+
function buildOutput(name, fields, description) {
|
|
109
|
+
return buildMessage(name, 'output', fields, description);
|
|
110
|
+
}
|
|
111
|
+
function buildQuery(name, fields, description) {
|
|
112
|
+
return buildMessage(name, 'query', fields, description);
|
|
113
|
+
}
|
|
114
|
+
function buildPk(name, fields, description) {
|
|
115
|
+
return buildMessage(name, 'pk', fields, description);
|
|
116
|
+
}
|
|
117
|
+
/** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
|
|
118
|
+
function from(table, fields) {
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const field of fields) {
|
|
121
|
+
if (field.schema !== table) {
|
|
122
|
+
throw new Error(`dto.from(${table.name}): field ${field.name} does not belong to this table`);
|
|
123
|
+
}
|
|
124
|
+
out[field.name] = dtoField(field);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderEnum = renderEnum;
|
|
4
|
+
// Enum driver: renders an EnumField into a standalone TypeScript enum file.
|
|
5
|
+
// Shape matches the generated-enum product consumed by the TypeBox driver (Type.Enum):
|
|
6
|
+
//
|
|
7
|
+
// export enum UserStatus {
|
|
8
|
+
// ACTIVE = 'ACTIVE',
|
|
9
|
+
// DISABLED = 'DISABLED',
|
|
10
|
+
// }
|
|
11
|
+
//
|
|
12
|
+
// export const USER_STATUS_LABEL: Record<UserStatus, string> = {
|
|
13
|
+
// [UserStatus.ACTIVE]: '启用',
|
|
14
|
+
// [UserStatus.DISABLED]: '禁用',
|
|
15
|
+
// };
|
|
16
|
+
function renderString(s) {
|
|
17
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
+
}
|
|
19
|
+
function renderValue(value) {
|
|
20
|
+
return typeof value === 'number' ? String(value) : renderString(value);
|
|
21
|
+
}
|
|
22
|
+
/** 'UserStatus' → 'USER_STATUS_LABEL' (matches the label map naming convention) */
|
|
23
|
+
function labelName(jsName) {
|
|
24
|
+
return `${jsName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_LABEL`;
|
|
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)},`);
|
|
32
|
+
return [
|
|
33
|
+
`export enum ${name} {`,
|
|
34
|
+
...members,
|
|
35
|
+
'}',
|
|
36
|
+
'',
|
|
37
|
+
`export const ${labelName(name)}: Record<${name}, string> = {`,
|
|
38
|
+
...labels,
|
|
39
|
+
'};',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./dsl"), exports);
|
|
18
|
+
__exportStar(require("./dto"), exports);
|
|
19
|
+
__exportStar(require("./mysql-driver"), exports);
|
|
20
|
+
__exportStar(require("./enum-driver"), exports);
|
|
21
|
+
__exportStar(require("./typebox-driver"), exports);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildCreateTableSql = buildCreateTableSql;
|
|
4
|
+
// MySQL driver: converts a TableSchema into a CREATE TABLE statement.
|
|
5
|
+
function columnType(field) {
|
|
6
|
+
switch (field.type) {
|
|
7
|
+
case 'string':
|
|
8
|
+
if (!field.maxLength)
|
|
9
|
+
throw new Error(`field ${field.name} (string) requires maxLength`);
|
|
10
|
+
return `VARCHAR(${field.maxLength})`;
|
|
11
|
+
case 'text':
|
|
12
|
+
return 'TEXT';
|
|
13
|
+
case 'integer':
|
|
14
|
+
return 'INT';
|
|
15
|
+
case 'bigint':
|
|
16
|
+
return 'BIGINT';
|
|
17
|
+
case 'decimal':
|
|
18
|
+
return `DECIMAL(${field.precision}, ${field.scale})`;
|
|
19
|
+
case 'boolean':
|
|
20
|
+
return 'TINYINT(1)';
|
|
21
|
+
case 'date':
|
|
22
|
+
return 'DATE';
|
|
23
|
+
case 'time':
|
|
24
|
+
return 'TIME';
|
|
25
|
+
case 'datetime':
|
|
26
|
+
return 'DATETIME';
|
|
27
|
+
case 'enum':
|
|
28
|
+
// Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT.
|
|
29
|
+
return field.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
|
|
30
|
+
case 'json':
|
|
31
|
+
return 'JSON';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function columnDef(field) {
|
|
35
|
+
const parts = [field.name, columnType(field)];
|
|
36
|
+
if (field.optional === false)
|
|
37
|
+
parts.push('NOT NULL');
|
|
38
|
+
if (field.default !== undefined)
|
|
39
|
+
parts.push(`DEFAULT '${field.default}'`);
|
|
40
|
+
return parts.join(' ');
|
|
41
|
+
}
|
|
42
|
+
function primaryKeyClause(schema) {
|
|
43
|
+
if (!schema.primaryKey)
|
|
44
|
+
return null;
|
|
45
|
+
const fields = Array.isArray(schema.primaryKey) ? schema.primaryKey : [schema.primaryKey];
|
|
46
|
+
return `PRIMARY KEY (${fields.map((f) => f.name).join(', ')})`;
|
|
47
|
+
}
|
|
48
|
+
function indexClause(index) {
|
|
49
|
+
const fields = Array.isArray(index.fields) ? index.fields : [index.fields];
|
|
50
|
+
const kind = index.unique ? 'UNIQUE KEY' : 'KEY';
|
|
51
|
+
const name = index.name ?? fields.map((f) => f.name).join('_');
|
|
52
|
+
return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
|
|
53
|
+
}
|
|
54
|
+
function buildCreateTableSql(schema) {
|
|
55
|
+
const lines = Object.values(schema.fields).map(columnDef);
|
|
56
|
+
const pk = primaryKeyClause(schema);
|
|
57
|
+
if (pk)
|
|
58
|
+
lines.push(pk);
|
|
59
|
+
for (const index of schema.indexes ?? [])
|
|
60
|
+
lines.push(indexClause(index));
|
|
61
|
+
return `CREATE TABLE \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
|
|
62
|
+
}
|