@pylonts/dsl 1.0.5 → 1.1.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 +2 -1
- package/dist/asset.d.ts +48 -0
- package/dist/asset.js +31 -0
- package/dist/bases.d.ts +6 -2
- package/dist/bases.js +10 -6
- package/dist/check-inheritance.js +1 -4
- package/dist/curd.d.ts +60 -0
- package/dist/curd.js +31 -0
- package/dist/db-config.d.ts +8 -0
- package/dist/db-config.js +1 -0
- package/dist/db.d.ts +52 -0
- package/dist/db.js +91 -0
- package/dist/dictionary.d.ts +26 -5
- package/dist/dictionary.js +21 -8
- package/dist/dsl.d.ts +5 -49
- package/dist/dsl.js +12 -103
- package/dist/dto.d.ts +14 -9
- package/dist/dto.js +28 -38
- package/dist/enum-driver.d.ts +1 -1
- package/dist/enum-driver.js +1 -4
- package/dist/flow.d.ts +1 -1
- package/dist/flow.js +3 -8
- package/dist/import-base.d.ts +15 -0
- package/dist/import-base.js +1 -0
- package/dist/index.d.ts +21 -17
- package/dist/index.js +21 -33
- package/dist/mermaid-driver.d.ts +2 -2
- package/dist/mermaid-driver.js +2 -6
- package/dist/mock.d.ts +30 -0
- package/dist/mock.js +18 -0
- package/dist/mysql-driver.d.ts +1 -1
- package/dist/mysql-driver.js +20 -10
- package/dist/page-flow.d.ts +2 -2
- package/dist/page-flow.js +2 -6
- package/dist/page.d.ts +6 -6
- package/dist/page.js +3 -8
- package/dist/pattern.js +2 -6
- package/dist/patterns/retry.d.ts +1 -1
- package/dist/patterns/retry.js +2 -6
- package/dist/project.d.ts +18 -13
- package/dist/project.js +41 -6
- package/dist/prototype.d.ts +1 -1
- package/dist/prototype.js +1 -4
- package/dist/typebox-driver.d.ts +3 -3
- package/dist/typebox-driver.js +28 -24
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -4
- package/docs/curd.md +111 -0
- package/docs/dictionary.md +42 -31
- package/docs/driver.md +42 -42
- package/docs/dto.md +66 -66
- package/docs/enum.md +24 -24
- package/docs/project.md +2 -2
- package/docs/table.md +50 -15
- package/package.json +6 -4
- package/src/asset.ts +63 -0
- package/src/bases.ts +12 -2
- package/src/curd.ts +92 -0
- package/src/db-config.ts +8 -0
- package/src/db.ts +142 -0
- package/src/dictionary.ts +45 -19
- package/src/dsl.ts +182 -281
- package/src/dto.ts +247 -234
- package/src/enum-driver.ts +1 -1
- package/src/flow.ts +1 -1
- package/src/import-base.ts +15 -0
- package/src/index.ts +21 -17
- package/src/mermaid-driver.ts +3 -3
- package/src/mock.ts +45 -0
- package/src/mysql-driver.ts +19 -6
- package/src/page-flow.ts +3 -3
- package/src/page.ts +7 -7
- package/src/patterns/retry.ts +1 -1
- package/src/project.ts +90 -54
- package/src/prototype.ts +1 -1
- package/src/typebox-driver.ts +192 -183
- package/src/utils.ts +5 -0
- package/src/check-inheritance.ts +0 -86
package/src/db.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// DB table definitions.
|
|
2
|
+
// Shape: { type: 'table', <extension fields> }
|
|
3
|
+
|
|
4
|
+
import type { EntityPhrase } from './dictionary.js';
|
|
5
|
+
import type { CollectionSchemaBase, EnumDef, Field } from './dsl.js';
|
|
6
|
+
|
|
7
|
+
export type Index = {
|
|
8
|
+
name?: string;
|
|
9
|
+
columns: Field | Field[];
|
|
10
|
+
unique?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ForeignKey = {
|
|
14
|
+
columns: Field | Field[];
|
|
15
|
+
references: Field | Field[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface TableSchemaOptions<
|
|
19
|
+
N extends string = string,
|
|
20
|
+
C extends Record<string, Field> = Record<string, Field>,
|
|
21
|
+
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
22
|
+
> {
|
|
23
|
+
description?: string;
|
|
24
|
+
paginated?: boolean;
|
|
25
|
+
actor?: boolean;
|
|
26
|
+
generator?: string;
|
|
27
|
+
autoIncrement?: Field;
|
|
28
|
+
primaryKey?: Field | Field[];
|
|
29
|
+
indexes?: Index[];
|
|
30
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
31
|
+
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
32
|
+
phrase?: EntityPhrase;
|
|
33
|
+
/** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
|
|
34
|
+
enums: E;
|
|
35
|
+
columns: C;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class TableSchema<
|
|
39
|
+
N extends string = string,
|
|
40
|
+
C extends Record<string, Field> = Record<string, Field>,
|
|
41
|
+
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
42
|
+
> implements CollectionSchemaBase {
|
|
43
|
+
type = 'table';
|
|
44
|
+
name: N;
|
|
45
|
+
description?: string;
|
|
46
|
+
/** 分页 */
|
|
47
|
+
paginated?: boolean;
|
|
48
|
+
/** 系统操作者(如小程序为 C 端用户,管理端为运营) */
|
|
49
|
+
actor?: boolean;
|
|
50
|
+
/** id 生成器 */
|
|
51
|
+
generator?: string;
|
|
52
|
+
/** 自增主键字段 */
|
|
53
|
+
autoIncrement?: Field;
|
|
54
|
+
primaryKey?: Field | Field[];
|
|
55
|
+
indexes?: Index[];
|
|
56
|
+
/** 外键,引用其他表的字段 */
|
|
57
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
58
|
+
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
59
|
+
phrase?: EntityPhrase;
|
|
60
|
+
/** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
|
|
61
|
+
enums: E;
|
|
62
|
+
columns: C;
|
|
63
|
+
|
|
64
|
+
constructor(name: N, options: TableSchemaOptions<N, C, E>) {
|
|
65
|
+
if (!options.enums) {
|
|
66
|
+
throw new Error(`table '${name}': enums is required — declare enums: {} when the table has no enums`);
|
|
67
|
+
}
|
|
68
|
+
this.name = name;
|
|
69
|
+
this.description = options.description;
|
|
70
|
+
this.paginated = options.paginated;
|
|
71
|
+
this.actor = options.actor;
|
|
72
|
+
this.generator = options.generator;
|
|
73
|
+
this.autoIncrement = options.autoIncrement;
|
|
74
|
+
this.primaryKey = options.primaryKey;
|
|
75
|
+
this.indexes = options.indexes;
|
|
76
|
+
this.foreignKeys = options.foreignKeys;
|
|
77
|
+
this.phrase = options.phrase;
|
|
78
|
+
this.enums = options.enums;
|
|
79
|
+
this.columns = options.columns;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** True when the field is part of this table's primary key */
|
|
83
|
+
isPk(fieldRef: Field): boolean {
|
|
84
|
+
if (this.primaryKey === undefined) return false;
|
|
85
|
+
return Array.isArray(this.primaryKey)
|
|
86
|
+
? this.primaryKey.includes(fieldRef)
|
|
87
|
+
: this.primaryKey === fieldRef;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function defineTable<
|
|
92
|
+
N extends string,
|
|
93
|
+
C extends Record<string, Field>,
|
|
94
|
+
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
95
|
+
>(
|
|
96
|
+
name: N,
|
|
97
|
+
schema: TableSchemaOptions<N, C, E>,
|
|
98
|
+
): TableSchema<N, C, E> {
|
|
99
|
+
const table = new TableSchema<N, C, E>(name, schema);
|
|
100
|
+
for (const key of Object.keys(table.columns)) {
|
|
101
|
+
const field = table.columns[key] as Field;
|
|
102
|
+
if (field.schema && field.schema !== table) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const key of Object.keys(table.columns)) {
|
|
109
|
+
table.columns[key].name = key;
|
|
110
|
+
table.columns[key].schema = table;
|
|
111
|
+
}
|
|
112
|
+
for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
|
|
113
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
114
|
+
const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
|
|
115
|
+
for (let i = 0; i < refs.length; i++) {
|
|
116
|
+
const ref = refs[i];
|
|
117
|
+
if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
|
|
118
|
+
if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
|
|
119
|
+
const phrase = (ref.schema as TableSchema).phrase;
|
|
120
|
+
if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
|
|
121
|
+
const expected = `${phrase.name}_${ref.name}`;
|
|
122
|
+
const fkField = fields[i];
|
|
123
|
+
if (fkField.name !== expected) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [idxKey, index] of Object.entries(table.indexes ?? {})) {
|
|
131
|
+
const cols = Array.isArray(index.columns) ? index.columns : [index.columns];
|
|
132
|
+
for (const col of cols) {
|
|
133
|
+
if (typeof col !== 'object' || col === null || typeof (col as Field).type !== 'string') {
|
|
134
|
+
const idxName = index.name ?? idxKey;
|
|
135
|
+
throw new Error(
|
|
136
|
+
`index ${idxName}: columns must be Field instances (stringField()/intField()/...), got ${JSON.stringify(col)}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return table;
|
|
142
|
+
}
|
package/src/dictionary.ts
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl';
|
|
2
|
-
|
|
3
|
-
// Dictionary definitions: vocabulary shared across the team — what a term
|
|
4
|
-
// means and what it is called.
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
|
|
3
|
+
// Dictionary definitions: vocabulary shared across the team — what a term
|
|
4
|
+
// means and what it is called.
|
|
5
|
+
|
|
6
|
+
/** Kind of a dictionary entry: which naming rule the phrase participates in. */
|
|
7
|
+
export enum DictionaryEntryType {
|
|
8
|
+
Entity = 'entity',
|
|
9
|
+
Business = 'business',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A vocabulary entry. */
|
|
13
|
+
export interface DictionaryEntry extends SchemaBase {
|
|
14
|
+
/**
|
|
15
|
+
* Entry kind. Entity phrases (abbreviation of an entity, e.g. mer) must be
|
|
16
|
+
* the FIRST segment of a field name; business phrases (attribute of an
|
|
17
|
+
* entity, e.g. rate) must be the LAST segment.
|
|
18
|
+
*/
|
|
19
|
+
type: DictionaryEntryType;
|
|
20
|
+
/** Display label (Chinese) for the term. */
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** An entity phrase entry — type narrowed to Entity. */
|
|
25
|
+
export interface EntityPhrase extends DictionaryEntry {
|
|
26
|
+
type: DictionaryEntryType.Entity;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Explains an entity phrase — the returned entry IS the phrase. `name` is the
|
|
31
|
+
* phrase itself (column-name stem, e.g. mer / bd), not a full entity name;
|
|
32
|
+
* label/description explain what it means. Convention: short phrase, not long
|
|
33
|
+
* name. Entity phrases appear as the first segment of a field name (mer_id).
|
|
34
|
+
*/
|
|
35
|
+
export function defineEntityPhrase(extra: Omit<DictionaryEntry, 'type'>): EntityPhrase {
|
|
36
|
+
return { type: DictionaryEntryType.Entity, ...extra };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Explains a business phrase (an attribute of an entity) — the returned entry
|
|
41
|
+
* IS the phrase. `name` is the phrase itself (column-name stem, e.g. amt /
|
|
42
|
+
* rate). Business phrases appear as the last segment of a field name (bd_rate).
|
|
43
|
+
*/
|
|
44
|
+
export function defineBusinessPhrase(extra: Omit<DictionaryEntry, 'type'>): DictionaryEntry {
|
|
45
|
+
return { type: DictionaryEntryType.Business, ...extra };
|
|
20
46
|
}
|
package/src/dsl.ts
CHANGED
|
@@ -1,282 +1,183 @@
|
|
|
1
|
-
// DSL field type definitions.
|
|
2
|
-
// Shape: { type: <type name>, <extension fields> }
|
|
3
|
-
|
|
4
|
-
import type { DictionaryEntry } from './dictionary';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export
|
|
157
|
-
type
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.primaryKey = options.primaryKey;
|
|
184
|
-
this.indexes = options.indexes;
|
|
185
|
-
this.foreignKeys = options.foreignKeys;
|
|
186
|
-
this.phrase = options.phrase;
|
|
187
|
-
this.fields = options.fields;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** True when the field is part of this table's primary key */
|
|
191
|
-
isPk(fieldRef: Field): boolean {
|
|
192
|
-
if (this.primaryKey === undefined) return false;
|
|
193
|
-
return Array.isArray(this.primaryKey)
|
|
194
|
-
? this.primaryKey.includes(fieldRef)
|
|
195
|
-
: this.primaryKey === fieldRef;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Field builders: type and jsType are fixed, pass extra properties only.
|
|
200
|
-
// The field name is written back from the map key later (see defineTable).
|
|
201
|
-
|
|
202
|
-
type FieldExtras<T extends Field> = Omit<T, 'name' | 'type' | 'jsType'>;
|
|
203
|
-
|
|
204
|
-
export function stringField(extra: FieldExtras<StringField> = {}): StringField {
|
|
205
|
-
return { name: '', type: 'string', jsType: 'string', ...extra };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function textField(extra: FieldExtras<TextField> = {}): TextField {
|
|
209
|
-
return { name: '', type: 'text', jsType: 'string', ...extra };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function intField(extra: FieldExtras<IntField> = {}): IntField {
|
|
213
|
-
return { name: '', type: 'integer', jsType: 'number', ...extra };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function bigintField(extra: FieldExtras<BigintField> = {}): BigintField {
|
|
217
|
-
return { name: '', type: 'bigint', jsType: 'string', ...extra };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function decimalField(extra: FieldExtras<DecimalField>): DecimalField {
|
|
221
|
-
return { name: '', type: 'decimal', jsType: 'string', ...extra };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function booleanField(extra: FieldExtras<BooleanField> = {}): BooleanField {
|
|
225
|
-
return { name: '', type: 'boolean', jsType: 'boolean', ...extra };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function dateField(extra: FieldExtras<DateField> = {}): DateField {
|
|
229
|
-
return { name: '', type: 'date', jsType: 'Date', ...extra };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function timeField(extra: FieldExtras<TimeField> = {}): TimeField {
|
|
233
|
-
return { name: '', type: 'time', jsType: 'string', ...extra };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function datetimeField(extra: FieldExtras<DateTimeField> = {}): DateTimeField {
|
|
237
|
-
return { name: '', type: 'datetime', jsType: 'Date', ...extra };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function jsonField(extra: FieldExtras<JsonField> = {}): JsonField {
|
|
241
|
-
return { name: '', type: 'json', jsType: 'object', ...extra };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
|
|
245
|
-
const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
|
|
246
|
-
return { name: '', type: 'enum', jsType, ...extra };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function defineTable(name: string, schema: TableSchemaOptions): TableSchema {
|
|
250
|
-
const table = new TableSchema(name, schema);
|
|
251
|
-
for (const key of Object.keys(table.fields)) {
|
|
252
|
-
const field = table.fields[key];
|
|
253
|
-
if (field.schema && field.schema !== table) {
|
|
254
|
-
throw new Error(
|
|
255
|
-
`field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
for (const key of Object.keys(table.fields)) {
|
|
260
|
-
table.fields[key].name = key;
|
|
261
|
-
table.fields[key].schema = table;
|
|
262
|
-
}
|
|
263
|
-
for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
|
|
264
|
-
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
265
|
-
const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
|
|
266
|
-
for (let i = 0; i < refs.length; i++) {
|
|
267
|
-
const ref = refs[i];
|
|
268
|
-
if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
|
|
269
|
-
if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
|
|
270
|
-
const phrase = (ref.schema as TableSchema).phrase;
|
|
271
|
-
if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
|
|
272
|
-
const expected = `${phrase.name}_${ref.name}`;
|
|
273
|
-
const fkField = fields[i];
|
|
274
|
-
if (fkField.name !== expected) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
`foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return table;
|
|
1
|
+
// DSL field type definitions.
|
|
2
|
+
// Shape: { type: <type name>, <extension fields> }
|
|
3
|
+
|
|
4
|
+
import type { DictionaryEntry } from './dictionary.js';
|
|
5
|
+
import type { MockDescriptor } from './mock.js';
|
|
6
|
+
|
|
7
|
+
/** Field query comparison operators (search field semantics). */
|
|
8
|
+
export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne';
|
|
9
|
+
|
|
10
|
+
export interface SchemaBase {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Field collection schemas (DB table vs DTO message); `type` is the discriminator */
|
|
16
|
+
export interface CollectionSchemaBase extends SchemaBase {
|
|
17
|
+
type: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BaseField {
|
|
21
|
+
name: string;
|
|
22
|
+
/** 显示名称(中文标签) */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** 字段描述 */
|
|
25
|
+
description?: string;
|
|
26
|
+
optional?: boolean;
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
default?: string;
|
|
29
|
+
/** 所属 schema(db 或 dto) */
|
|
30
|
+
schema?: CollectionSchemaBase;
|
|
31
|
+
/** Pure-data mock descriptor for test data generation */
|
|
32
|
+
mock?: MockDescriptor;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface StringField extends BaseField {
|
|
36
|
+
type: 'string';
|
|
37
|
+
jsType: 'string';
|
|
38
|
+
minLength?: number;
|
|
39
|
+
maxLength?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TextField extends BaseField {
|
|
43
|
+
type: 'text';
|
|
44
|
+
jsType: 'string';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface IntField extends BaseField {
|
|
48
|
+
type: 'integer';
|
|
49
|
+
jsType: 'number';
|
|
50
|
+
min?: number;
|
|
51
|
+
max?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BigintField extends BaseField {
|
|
55
|
+
type: 'bigint';
|
|
56
|
+
jsType: 'string';
|
|
57
|
+
// Value is transported as string to keep precision beyond 2^53.
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface DecimalField extends BaseField {
|
|
61
|
+
type: 'decimal';
|
|
62
|
+
jsType: 'string';
|
|
63
|
+
precision: number;
|
|
64
|
+
scale: number;
|
|
65
|
+
// Value is transported as string to avoid binary float error.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface BooleanField extends BaseField {
|
|
69
|
+
type: 'boolean';
|
|
70
|
+
jsType: 'boolean';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface DateField extends BaseField {
|
|
74
|
+
type: 'date';
|
|
75
|
+
jsType: 'Date';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface TimeField extends BaseField {
|
|
79
|
+
type: 'time';
|
|
80
|
+
jsType: 'string';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface DateTimeField extends BaseField {
|
|
84
|
+
type: 'datetime';
|
|
85
|
+
jsType: 'Date';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface EnumValue {
|
|
89
|
+
value: string | number;
|
|
90
|
+
symbol: string;
|
|
91
|
+
label: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Shared enum definition. Pure value object, safe to reference from multiple fields/tables. */
|
|
95
|
+
export interface EnumDef {
|
|
96
|
+
/** JS 定义名称,如 MerchantStatus */
|
|
97
|
+
jsName: string;
|
|
98
|
+
valueType: 'string' | 'integer';
|
|
99
|
+
values: EnumValue[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function defineEnum(
|
|
103
|
+
jsName: string,
|
|
104
|
+
valueType: 'string' | 'integer',
|
|
105
|
+
values: EnumValue[],
|
|
106
|
+
): EnumDef {
|
|
107
|
+
return { jsName, valueType, values };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface EnumField extends BaseField {
|
|
111
|
+
type: 'enum';
|
|
112
|
+
jsType: 'string' | 'number';
|
|
113
|
+
/** Reference to a shared enum definition (see defineEnum). */
|
|
114
|
+
enum: EnumDef;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface JsonField extends BaseField {
|
|
118
|
+
type: 'json';
|
|
119
|
+
jsType: 'object';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type Field =
|
|
123
|
+
| StringField
|
|
124
|
+
| TextField
|
|
125
|
+
| IntField
|
|
126
|
+
| BigintField
|
|
127
|
+
| DecimalField
|
|
128
|
+
| BooleanField
|
|
129
|
+
| DateField
|
|
130
|
+
| TimeField
|
|
131
|
+
| DateTimeField
|
|
132
|
+
| EnumField
|
|
133
|
+
| JsonField;
|
|
134
|
+
|
|
135
|
+
// Field builders: type and jsType are fixed, pass extra properties only.
|
|
136
|
+
// The field name is written back from the map key later (see defineTable).
|
|
137
|
+
|
|
138
|
+
type FieldExtras<T extends Field> = Omit<T, 'name' | 'type' | 'jsType'>;
|
|
139
|
+
|
|
140
|
+
export function stringField(extra: FieldExtras<StringField> = {}): StringField {
|
|
141
|
+
return { name: '', type: 'string', jsType: 'string', ...extra };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function textField(extra: FieldExtras<TextField> = {}): TextField {
|
|
145
|
+
return { name: '', type: 'text', jsType: 'string', ...extra };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function intField(extra: FieldExtras<IntField> = {}): IntField {
|
|
149
|
+
return { name: '', type: 'integer', jsType: 'number', ...extra };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function bigintField(extra: FieldExtras<BigintField> = {}): BigintField {
|
|
153
|
+
return { name: '', type: 'bigint', jsType: 'string', ...extra };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function decimalField(extra: FieldExtras<DecimalField>): DecimalField {
|
|
157
|
+
return { name: '', type: 'decimal', jsType: 'string', ...extra };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function booleanField(extra: FieldExtras<BooleanField> = {}): BooleanField {
|
|
161
|
+
return { name: '', type: 'boolean', jsType: 'boolean', ...extra };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function dateField(extra: FieldExtras<DateField> = {}): DateField {
|
|
165
|
+
return { name: '', type: 'date', jsType: 'Date', ...extra };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function timeField(extra: FieldExtras<TimeField> = {}): TimeField {
|
|
169
|
+
return { name: '', type: 'time', jsType: 'string', ...extra };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function datetimeField(extra: FieldExtras<DateTimeField> = {}): DateTimeField {
|
|
173
|
+
return { name: '', type: 'datetime', jsType: 'Date', ...extra };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function jsonField(extra: FieldExtras<JsonField> = {}): JsonField {
|
|
177
|
+
return { name: '', type: 'json', jsType: 'object', ...extra };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
|
|
181
|
+
const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
|
|
182
|
+
return { name: '', type: 'enum', jsType, ...extra };
|
|
282
183
|
}
|