@pylonts/dsl 1.1.1 → 1.1.3
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/dist/action.d.ts +7 -0
- package/dist/action.js +3 -0
- package/dist/asset.d.ts +2 -2
- package/dist/asset.js +2 -2
- package/dist/component.d.ts +20 -0
- package/dist/component.js +1 -0
- package/dist/convert.d.ts +9 -0
- package/dist/convert.js +3 -0
- package/dist/curd.d.ts +3 -1
- package/dist/db.d.ts +4 -0
- package/dist/db.js +29 -0
- package/dist/dsl.d.ts +5 -0
- package/dist/dto.d.ts +2 -2
- package/dist/dto.js +1 -1
- package/dist/event.d.ts +8 -0
- package/dist/event.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/mermaid-driver.js +2 -1
- package/dist/mock.d.ts +3 -21
- package/dist/mock.js +1 -18
- package/dist/mysql-driver.d.ts +4 -0
- package/dist/mysql-driver.js +8 -3
- package/dist/navigation.d.ts +22 -0
- package/dist/navigation.js +15 -0
- package/dist/page-def.d.ts +40 -0
- package/dist/page-def.js +38 -0
- package/dist/page-flow.d.ts +4 -2
- package/dist/page-flow.js +107 -12
- package/dist/page.d.ts +32 -10
- package/dist/page.js +20 -5
- package/dist/popup.d.ts +18 -0
- package/dist/popup.js +8 -0
- package/dist/project.d.ts +7 -0
- package/dist/provider.d.ts +54 -0
- package/dist/provider.js +18 -0
- package/dist/ref.d.ts +14 -0
- package/dist/ref.js +6 -0
- package/dist/route.d.ts +8 -0
- package/dist/route.js +3 -0
- package/docs/curd.md +110 -110
- package/docs/dto.md +66 -66
- package/docs/table.md +4 -2
- package/package.json +1 -1
- package/src/action.ts +11 -0
- package/src/asset.ts +63 -63
- package/src/bases.ts +29 -29
- package/src/component.ts +22 -0
- package/src/convert.ts +13 -0
- package/src/curd.ts +93 -91
- package/src/db.ts +37 -0
- package/src/dsl.ts +188 -182
- package/src/dto.ts +247 -247
- package/src/enum-driver.ts +42 -42
- package/src/event.ts +12 -0
- package/src/flow.ts +103 -103
- package/src/index.ts +31 -21
- package/src/mermaid-driver.ts +2 -1
- package/src/mock.ts +12 -45
- package/src/mysql-driver.ts +8 -2
- package/src/navigation.ts +29 -0
- package/src/page-def.ts +80 -0
- package/src/page-flow.ts +116 -14
- package/src/page.ts +51 -14
- package/src/patterns/retry.ts +54 -54
- package/src/popup.ts +25 -0
- package/src/project.ts +97 -90
- package/src/prototype.ts +29 -29
- package/src/provider.ts +73 -0
- package/src/ref.ts +19 -0
- package/src/route.ts +12 -0
- package/src/utils.ts +10 -10
package/src/dto.ts
CHANGED
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './dsl.js';
|
|
2
|
-
import { TableSchema } from './db.js';
|
|
3
|
-
import type { ImportBase } from './import-base.js';
|
|
4
|
-
import { toCamelCase } from './utils.js';
|
|
5
|
-
|
|
6
|
-
// Interface (DTO) field definitions.
|
|
7
|
-
// Naming convention: all DTO types and builders use the Dto prefix.
|
|
8
|
-
// A DtoField stores the database Field and the API-only extras separately.
|
|
9
|
-
|
|
10
|
-
/** Re-export — Operator lives on the DSL level (see dsl.ts). */
|
|
11
|
-
export type { Operator } from './dsl.js';
|
|
12
|
-
|
|
13
|
-
/** Re-export — ImportBase lives on its own module (see import-base.ts). */
|
|
14
|
-
export type { ImportBase } from './import-base.js';
|
|
15
|
-
|
|
16
|
-
/** Re-export — MockDescriptor lives on its own module (see mock.ts). */
|
|
17
|
-
export type { MockDescriptor } from './mock.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Reference to an existing TypeBox base schema by its import location.
|
|
21
|
-
* Serializable metadata: the driver renders `import { name } from 'from'`
|
|
22
|
-
* and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
|
|
23
|
-
*/
|
|
24
|
-
export interface ImportRef extends ImportBase {
|
|
25
|
-
/**
|
|
26
|
-
* Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
|
|
27
|
-
* Two forms, both local DTOs:
|
|
28
|
-
* string — the DTO export name
|
|
29
|
-
* DtoMessage — the DTO instance itself; the driver resolves it to its name
|
|
30
|
-
*/
|
|
31
|
-
args?: (string | DtoMessage)[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type DtoArrayFieldDef = BaseField & {
|
|
35
|
-
type: 'array';
|
|
36
|
-
jsType: 'array';
|
|
37
|
-
/** Element type: inline DtoField, or a reference to an existing DTO (rendered by name) */
|
|
38
|
-
items: DtoField | DtoMessage;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type DtoObjectFieldDef = BaseField & {
|
|
42
|
-
type: 'object';
|
|
43
|
-
jsType: 'object';
|
|
44
|
-
properties: Record<string, DtoField>;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export class DtoField implements SchemaBase {
|
|
48
|
-
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
49
|
-
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
50
|
-
name: string;
|
|
51
|
-
/** 字段描述 */
|
|
52
|
-
description?: string;
|
|
53
|
-
/**
|
|
54
|
-
schema?:
|
|
55
|
-
field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
|
|
56
|
-
pattern?: string;
|
|
57
|
-
optional?: boolean;
|
|
58
|
-
operator?: Operator;
|
|
59
|
-
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
60
|
-
default?: unknown;
|
|
61
|
-
|
|
62
|
-
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
|
|
63
|
-
this.name = '';
|
|
64
|
-
this.field = field;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
setPattern(value: string): this {
|
|
68
|
-
this.pattern = value;
|
|
69
|
-
return this;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
setDescription(value: string): this {
|
|
73
|
-
this.description = value;
|
|
74
|
-
return this;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
getDescription(): string | undefined {
|
|
78
|
-
return this.description;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** True when this field wraps a DB column (picked via from()); false for inline fields. */
|
|
82
|
-
isColumn(): boolean {
|
|
83
|
-
return this.field.schema?.type === 'table';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
setOptional(value: boolean): this {
|
|
87
|
-
this.optional = value;
|
|
88
|
-
return this;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Set a default value — emitted as a TypeBox schema default annotation */
|
|
92
|
-
setDefault(value: unknown): this {
|
|
93
|
-
this.default = value;
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
98
|
-
setOperator(value: Operator): this {
|
|
99
|
-
this.operator = value;
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** optional 优先于 field.optional */
|
|
104
|
-
isOptional(): boolean {
|
|
105
|
-
if (this.optional !== undefined) return this.optional;
|
|
106
|
-
return this.field.optional ?? false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export class DtoArrayField extends DtoField {
|
|
111
|
-
declare field: DtoArrayFieldDef;
|
|
112
|
-
|
|
113
|
-
/** Element type: inline DtoField or a referenced DtoMessage (rendered by name). */
|
|
114
|
-
items(): DtoField | DtoMessage {
|
|
115
|
-
return this.field.items;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export class DtoObjectField extends DtoField {
|
|
120
|
-
declare field: DtoObjectFieldDef;
|
|
121
|
-
|
|
122
|
-
properties(): Record<string, DtoField> {
|
|
123
|
-
return this.field.properties;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export enum DtoDirection {
|
|
128
|
-
Input = 'input',
|
|
129
|
-
Output = 'output',
|
|
130
|
-
Query = 'query',
|
|
131
|
-
Pk = 'pk',
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export class DtoMessage implements CollectionSchemaBase {
|
|
135
|
-
type = 'dto';
|
|
136
|
-
name: string;
|
|
137
|
-
description?: string;
|
|
138
|
-
/** 方向:输入或输出 */
|
|
139
|
-
direction: DtoDirection;
|
|
140
|
-
fields: Record<string, DtoField>;
|
|
141
|
-
/** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
|
|
142
|
-
bases: ImportRef[] = [];
|
|
143
|
-
|
|
144
|
-
constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
|
|
145
|
-
this.name = name;
|
|
146
|
-
this.direction = direction;
|
|
147
|
-
this.fields = fields;
|
|
148
|
-
this.description = description;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
|
|
152
|
-
include(...refs: ImportRef[]): this {
|
|
153
|
-
this.bases.push(...refs);
|
|
154
|
-
return this;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function dtoField(field: Field): DtoField {
|
|
159
|
-
return new DtoField(field);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
|
|
163
|
-
// Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
|
|
164
|
-
// referenced by name (the driver renders Type.Array(<DtoName>)).
|
|
165
|
-
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
|
|
169
|
-
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
173
|
-
const message = new DtoMessage(name, direction, fields, description);
|
|
174
|
-
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
175
|
-
// are created per DTO, never shared).
|
|
176
|
-
for (const key of Object.keys(message.fields)) {
|
|
177
|
-
const df = message.fields[key];
|
|
178
|
-
if (!(df instanceof DtoField)) {
|
|
179
|
-
const got = df === null || df === undefined ? String(df) : `${(df as object).constructor.name ?? typeof df}`;
|
|
180
|
-
throw new Error(
|
|
181
|
-
`[dto] DTO "${name}" field "${key}" must be a DtoField (created with dtoField()/dtoArrayField()/dtoObjectField()), got ${got}. ` +
|
|
182
|
-
`Did you pass enumField(...) directly? Use dtoField(enumField({...})) instead.`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
df.name = key;
|
|
186
|
-
df.schema = message;
|
|
187
|
-
// Custom (inline) fields are owned by this DTO: write back name + schema.
|
|
188
|
-
// Fields picked via from() share the database Field instance whose
|
|
189
|
-
// name/schema already point to the table — leave them untouched.
|
|
190
|
-
if (df.field.schema === undefined) {
|
|
191
|
-
df.field.name = key;
|
|
192
|
-
df.field.schema = message;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return message;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
199
|
-
const message = buildMessage(name, DtoDirection.Input, fields, description);
|
|
200
|
-
// Rule A — set optionality from the DB column rule (skips fields the author
|
|
201
|
-
// already set): nullable / default → optional, else required.
|
|
202
|
-
// PK columns are always required.
|
|
203
|
-
for (const field of Object.values(message.fields)) {
|
|
204
|
-
if (field.optional !== undefined) continue;
|
|
205
|
-
const f = field.field as Field;
|
|
206
|
-
if (f.schema?.type !== 'table') continue;
|
|
207
|
-
const table = f.schema as TableSchema;
|
|
208
|
-
field.optional = table.isPk(f) ? false : f.optional !== false || f.default !== undefined;
|
|
209
|
-
}
|
|
210
|
-
return message;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
214
|
-
return buildMessage(name, DtoDirection.Output, fields, description);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
218
|
-
const message = buildMessage(name, DtoDirection.Query, fields, description);
|
|
219
|
-
// Rule B: query/search fields are always optional.
|
|
220
|
-
for (const field of Object.values(message.fields)) field.optional = true;
|
|
221
|
-
return message;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
225
|
-
const message = buildMessage(name, DtoDirection.Pk, fields, description);
|
|
226
|
-
// Rule P: PK locator fields are required, other fields are optional.
|
|
227
|
-
for (const field of Object.values(message.fields)) {
|
|
228
|
-
if (field.optional !== undefined) continue;
|
|
229
|
-
const f = field.field as Field;
|
|
230
|
-
const table = f.schema as TableSchema | undefined;
|
|
231
|
-
field.optional = table !== undefined && table.isPk(f) ? false : true;
|
|
232
|
-
}
|
|
233
|
-
return message;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
|
|
237
|
-
export function from(table: TableSchema, fields: Field[]): Record<string, DtoField> {
|
|
238
|
-
const out: Record<string, DtoField> = {};
|
|
239
|
-
for (const field of fields) {
|
|
240
|
-
if (field.schema !== table) {
|
|
241
|
-
throw new Error(`dto.from(${table.name}): field ${field.name} does not belong to this table`);
|
|
242
|
-
}
|
|
243
|
-
// DTO field name is camelCase (mer_id → merId); the underlying field.name
|
|
244
|
-
// stays snake_case (DB column).
|
|
245
|
-
out[toCamelCase(field.name)] = dtoField(field);
|
|
246
|
-
}
|
|
247
|
-
return out;
|
|
1
|
+
import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './dsl.js';
|
|
2
|
+
import { TableSchema } from './db.js';
|
|
3
|
+
import type { ImportBase } from './import-base.js';
|
|
4
|
+
import { toCamelCase } from './utils.js';
|
|
5
|
+
|
|
6
|
+
// Interface (DTO) field definitions.
|
|
7
|
+
// Naming convention: all DTO types and builders use the Dto prefix.
|
|
8
|
+
// A DtoField stores the database Field and the API-only extras separately.
|
|
9
|
+
|
|
10
|
+
/** Re-export — Operator lives on the DSL level (see dsl.ts). */
|
|
11
|
+
export type { Operator } from './dsl.js';
|
|
12
|
+
|
|
13
|
+
/** Re-export — ImportBase lives on its own module (see import-base.ts). */
|
|
14
|
+
export type { ImportBase } from './import-base.js';
|
|
15
|
+
|
|
16
|
+
/** Re-export — MockDescriptor lives on its own module (see mock.ts). */
|
|
17
|
+
export type { MockDescriptor } from './mock.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reference to an existing TypeBox base schema by its import location.
|
|
21
|
+
* Serializable metadata: the driver renders `import { name } from 'from'`
|
|
22
|
+
* and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
|
|
23
|
+
*/
|
|
24
|
+
export interface ImportRef extends ImportBase {
|
|
25
|
+
/**
|
|
26
|
+
* Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
|
|
27
|
+
* Two forms, both local DTOs:
|
|
28
|
+
* string — the DTO export name
|
|
29
|
+
* DtoMessage — the DTO instance itself; the driver resolves it to its name
|
|
30
|
+
*/
|
|
31
|
+
args?: (string | DtoMessage)[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DtoArrayFieldDef = BaseField & {
|
|
35
|
+
type: 'array';
|
|
36
|
+
jsType: 'array';
|
|
37
|
+
/** Element type: inline DtoField, or a reference to an existing DTO (rendered by name) */
|
|
38
|
+
items: DtoField | DtoMessage;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type DtoObjectFieldDef = BaseField & {
|
|
42
|
+
type: 'object';
|
|
43
|
+
jsType: 'object';
|
|
44
|
+
properties: Record<string, DtoField>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class DtoField implements SchemaBase {
|
|
48
|
+
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
49
|
+
* 构造时未知,由 buildMessage 从 map key 反写。 */
|
|
50
|
+
name: string;
|
|
51
|
+
/** 字段描述 */
|
|
52
|
+
description?: string;
|
|
53
|
+
/** 所属容器(buildMessage / defineRouteData / definePageData 反写) */
|
|
54
|
+
schema?: CollectionSchemaBase;
|
|
55
|
+
field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
|
|
56
|
+
pattern?: string;
|
|
57
|
+
optional?: boolean;
|
|
58
|
+
operator?: Operator;
|
|
59
|
+
/** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
|
|
60
|
+
default?: unknown;
|
|
61
|
+
|
|
62
|
+
constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
|
|
63
|
+
this.name = '';
|
|
64
|
+
this.field = field;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setPattern(value: string): this {
|
|
68
|
+
this.pattern = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setDescription(value: string): this {
|
|
73
|
+
this.description = value;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getDescription(): string | undefined {
|
|
78
|
+
return this.description;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when this field wraps a DB column (picked via from()); false for inline fields. */
|
|
82
|
+
isColumn(): boolean {
|
|
83
|
+
return this.field.schema?.type === 'table';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setOptional(value: boolean): this {
|
|
87
|
+
this.optional = value;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Set a default value — emitted as a TypeBox schema default annotation */
|
|
92
|
+
setDefault(value: unknown): this {
|
|
93
|
+
this.default = value;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
|
|
98
|
+
setOperator(value: Operator): this {
|
|
99
|
+
this.operator = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** optional 优先于 field.optional */
|
|
104
|
+
isOptional(): boolean {
|
|
105
|
+
if (this.optional !== undefined) return this.optional;
|
|
106
|
+
return this.field.optional ?? false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class DtoArrayField extends DtoField {
|
|
111
|
+
declare field: DtoArrayFieldDef;
|
|
112
|
+
|
|
113
|
+
/** Element type: inline DtoField or a referenced DtoMessage (rendered by name). */
|
|
114
|
+
items(): DtoField | DtoMessage {
|
|
115
|
+
return this.field.items;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class DtoObjectField extends DtoField {
|
|
120
|
+
declare field: DtoObjectFieldDef;
|
|
121
|
+
|
|
122
|
+
properties(): Record<string, DtoField> {
|
|
123
|
+
return this.field.properties;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export enum DtoDirection {
|
|
128
|
+
Input = 'input',
|
|
129
|
+
Output = 'output',
|
|
130
|
+
Query = 'query',
|
|
131
|
+
Pk = 'pk',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class DtoMessage implements CollectionSchemaBase {
|
|
135
|
+
type = 'dto';
|
|
136
|
+
name: string;
|
|
137
|
+
description?: string;
|
|
138
|
+
/** 方向:输入或输出 */
|
|
139
|
+
direction: DtoDirection;
|
|
140
|
+
fields: Record<string, DtoField>;
|
|
141
|
+
/** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
|
|
142
|
+
bases: ImportRef[] = [];
|
|
143
|
+
|
|
144
|
+
constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
|
|
145
|
+
this.name = name;
|
|
146
|
+
this.direction = direction;
|
|
147
|
+
this.fields = fields;
|
|
148
|
+
this.description = description;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
|
|
152
|
+
include(...refs: ImportRef[]): this {
|
|
153
|
+
this.bases.push(...refs);
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function dtoField(field: Field): DtoField {
|
|
159
|
+
return new DtoField(field);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
|
|
163
|
+
// Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
|
|
164
|
+
// referenced by name (the driver renders Type.Array(<DtoName>)).
|
|
165
|
+
return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
|
|
169
|
+
return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
173
|
+
const message = new DtoMessage(name, direction, fields, description);
|
|
174
|
+
// Write back the DTO field name from the map key (safe: DtoField instances
|
|
175
|
+
// are created per DTO, never shared).
|
|
176
|
+
for (const key of Object.keys(message.fields)) {
|
|
177
|
+
const df = message.fields[key];
|
|
178
|
+
if (!(df instanceof DtoField)) {
|
|
179
|
+
const got = df === null || df === undefined ? String(df) : `${(df as object).constructor.name ?? typeof df}`;
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[dto] DTO "${name}" field "${key}" must be a DtoField (created with dtoField()/dtoArrayField()/dtoObjectField()), got ${got}. ` +
|
|
182
|
+
`Did you pass enumField(...) directly? Use dtoField(enumField({...})) instead.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
df.name = key;
|
|
186
|
+
df.schema = message;
|
|
187
|
+
// Custom (inline) fields are owned by this DTO: write back name + schema.
|
|
188
|
+
// Fields picked via from() share the database Field instance whose
|
|
189
|
+
// name/schema already point to the table — leave them untouched.
|
|
190
|
+
if (df.field.schema === undefined) {
|
|
191
|
+
df.field.name = key;
|
|
192
|
+
df.field.schema = message;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return message;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
199
|
+
const message = buildMessage(name, DtoDirection.Input, fields, description);
|
|
200
|
+
// Rule A — set optionality from the DB column rule (skips fields the author
|
|
201
|
+
// already set): nullable / default → optional, else required.
|
|
202
|
+
// PK columns are always required.
|
|
203
|
+
for (const field of Object.values(message.fields)) {
|
|
204
|
+
if (field.optional !== undefined) continue;
|
|
205
|
+
const f = field.field as Field;
|
|
206
|
+
if (f.schema?.type !== 'table') continue;
|
|
207
|
+
const table = f.schema as TableSchema;
|
|
208
|
+
field.optional = table.isPk(f) ? false : f.optional !== false || f.default !== undefined;
|
|
209
|
+
}
|
|
210
|
+
return message;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
214
|
+
return buildMessage(name, DtoDirection.Output, fields, description);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
218
|
+
const message = buildMessage(name, DtoDirection.Query, fields, description);
|
|
219
|
+
// Rule B: query/search fields are always optional.
|
|
220
|
+
for (const field of Object.values(message.fields)) field.optional = true;
|
|
221
|
+
return message;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
|
|
225
|
+
const message = buildMessage(name, DtoDirection.Pk, fields, description);
|
|
226
|
+
// Rule P: PK locator fields are required, other fields are optional.
|
|
227
|
+
for (const field of Object.values(message.fields)) {
|
|
228
|
+
if (field.optional !== undefined) continue;
|
|
229
|
+
const f = field.field as Field;
|
|
230
|
+
const table = f.schema as TableSchema | undefined;
|
|
231
|
+
field.optional = table !== undefined && table.isPk(f) ? false : true;
|
|
232
|
+
}
|
|
233
|
+
return message;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
|
|
237
|
+
export function from(table: TableSchema, fields: Field[]): Record<string, DtoField> {
|
|
238
|
+
const out: Record<string, DtoField> = {};
|
|
239
|
+
for (const field of fields) {
|
|
240
|
+
if (field.schema !== table) {
|
|
241
|
+
throw new Error(`dto.from(${table.name}): field ${field.name} does not belong to this table`);
|
|
242
|
+
}
|
|
243
|
+
// DTO field name is camelCase (mer_id → merId); the underlying field.name
|
|
244
|
+
// stays snake_case (DB column).
|
|
245
|
+
out[toCamelCase(field.name)] = dtoField(field);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
248
|
}
|
package/src/enum-driver.ts
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { EnumDef } from './dsl.js';
|
|
2
|
-
|
|
3
|
-
// Enum driver: renders an EnumDef into a standalone TypeScript enum file.
|
|
4
|
-
// Shape matches the generated-enum product consumed by the TypeBox driver (Type.Enum):
|
|
5
|
-
//
|
|
6
|
-
// export enum UserStatus {
|
|
7
|
-
// ACTIVE = 'ACTIVE',
|
|
8
|
-
// DISABLED = 'DISABLED',
|
|
9
|
-
// }
|
|
10
|
-
//
|
|
11
|
-
// export const USER_STATUS_LABEL: Record<UserStatus, string> = {
|
|
12
|
-
// [UserStatus.ACTIVE]: '启用',
|
|
13
|
-
// [UserStatus.DISABLED]: '禁用',
|
|
14
|
-
// };
|
|
15
|
-
|
|
16
|
-
function renderString(s: string): string {
|
|
17
|
-
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderValue(value: string | number): string {
|
|
21
|
-
return typeof value === 'number' ? String(value) : renderString(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** 'UserStatus' → 'USER_STATUS_LABEL' (matches the label map naming convention) */
|
|
25
|
-
function labelName(jsName: string): string {
|
|
26
|
-
return `${jsName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_LABEL`;
|
|
27
|
-
}
|
|
28
|
-
|
|
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)},`);
|
|
33
|
-
return [
|
|
34
|
-
`export enum ${name} {`,
|
|
35
|
-
...members,
|
|
36
|
-
'}',
|
|
37
|
-
'',
|
|
38
|
-
`export const ${labelName(name)}: Record<${name}, string> = {`,
|
|
39
|
-
...labels,
|
|
40
|
-
'};',
|
|
41
|
-
'',
|
|
42
|
-
].join('\n');
|
|
1
|
+
import { EnumDef } from './dsl.js';
|
|
2
|
+
|
|
3
|
+
// Enum driver: renders an EnumDef into a standalone TypeScript enum file.
|
|
4
|
+
// Shape matches the generated-enum product consumed by the TypeBox driver (Type.Enum):
|
|
5
|
+
//
|
|
6
|
+
// export enum UserStatus {
|
|
7
|
+
// ACTIVE = 'ACTIVE',
|
|
8
|
+
// DISABLED = 'DISABLED',
|
|
9
|
+
// }
|
|
10
|
+
//
|
|
11
|
+
// export const USER_STATUS_LABEL: Record<UserStatus, string> = {
|
|
12
|
+
// [UserStatus.ACTIVE]: '启用',
|
|
13
|
+
// [UserStatus.DISABLED]: '禁用',
|
|
14
|
+
// };
|
|
15
|
+
|
|
16
|
+
function renderString(s: string): string {
|
|
17
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderValue(value: string | number): string {
|
|
21
|
+
return typeof value === 'number' ? String(value) : renderString(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 'UserStatus' → 'USER_STATUS_LABEL' (matches the label map naming convention) */
|
|
25
|
+
function labelName(jsName: string): string {
|
|
26
|
+
return `${jsName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_LABEL`;
|
|
27
|
+
}
|
|
28
|
+
|
|
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)},`);
|
|
33
|
+
return [
|
|
34
|
+
`export enum ${name} {`,
|
|
35
|
+
...members,
|
|
36
|
+
'}',
|
|
37
|
+
'',
|
|
38
|
+
`export const ${labelName(name)}: Record<${name}, string> = {`,
|
|
39
|
+
...labels,
|
|
40
|
+
'};',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
43
|
}
|
package/src/event.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CollectionSchemaBase } from './dsl.js';
|
|
2
|
+
import type { DtoField } from './dto.js';
|
|
3
|
+
|
|
4
|
+
/** Data carried by a component event (e.g. e.detail from a Tab onChange). */
|
|
5
|
+
export interface EventDataSchema extends CollectionSchemaBase {
|
|
6
|
+
type: 'event';
|
|
7
|
+
fields: Record<string, DtoField>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function defineEventData(name: string, fields: Record<string, DtoField>): EventDataSchema {
|
|
11
|
+
return { name, type: 'event', fields };
|
|
12
|
+
}
|