@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 +17 -85
- package/dist/dictionary.d.ts +25 -0
- package/dist/dictionary.js +45 -0
- package/dist/dsl.d.ts +17 -5
- package/dist/dsl.js +24 -4
- package/dist/enum-driver.d.ts +2 -2
- package/dist/enum-driver.js +5 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mysql-driver.d.ts +5 -1
- package/dist/mysql-driver.js +21 -4
- package/dist/project.d.ts +30 -0
- package/dist/project.js +11 -0
- package/dist/prototype.d.ts +18 -0
- package/dist/prototype.js +10 -0
- package/dist/typebox-driver.js +4 -8
- package/package.json +1 -1
- package/src/dictionary.ts +63 -0
- package/src/dsl.ts +48 -8
- package/src/enum-driver.ts +7 -8
- package/src/index.ts +4 -1
- package/src/mysql-driver.ts +26 -5
- package/src/project.ts +43 -0
- package/src/prototype.ts +30 -0
- package/src/typebox-driver.ts +4 -6
package/README.md
CHANGED
|
@@ -1,94 +1,26 @@
|
|
|
1
1
|
# @pylonts/dsl
|
|
2
2
|
|
|
3
|
-
Schema 定义与产物生成分离的 DSL 系统:先写 DSL 元数据(表 / DTO
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/enum-driver.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function renderEnum(
|
|
1
|
+
import { EnumDef } from './dsl';
|
|
2
|
+
export declare function renderEnum(def: EnumDef): string;
|
package/dist/enum-driver.js
CHANGED
|
@@ -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
|
|
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(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
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
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);
|
package/dist/mysql-driver.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import { TableSchema } from './dsl';
|
|
2
|
-
export
|
|
2
|
+
export interface BuildCreateTableSqlOptions {
|
|
3
|
+
/** 是否生成外键约束,默认 false(不生成) */
|
|
4
|
+
generateForeignKeys?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function buildCreateTableSql(schema: TableSchema, options?: BuildCreateTableSqlOptions): string;
|
package/dist/mysql-driver.js
CHANGED
|
@@ -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
|
|
55
|
-
const
|
|
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;
|
package/dist/project.js
ADDED
|
@@ -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
|
+
}
|
package/dist/typebox-driver.js
CHANGED
|
@@ -43,11 +43,9 @@ function renderBasic(field, pattern, resolver) {
|
|
|
43
43
|
case 'json':
|
|
44
44
|
return 'Type.Unknown()';
|
|
45
45
|
case 'enum': {
|
|
46
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/src/enum-driver.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EnumDef } from './dsl';
|
|
2
2
|
|
|
3
|
-
// Enum driver: renders an
|
|
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(
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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';
|
package/src/mysql-driver.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
const
|
|
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
|
+
}
|
package/src/prototype.ts
ADDED
|
@@ -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
|
+
}
|
package/src/typebox-driver.ts
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
}
|