@pylonts/dsl 1.0.3 → 1.0.4

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/docs/dto.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # 定义 DTO(四种方向)
2
2
 
3
- DTO 描述接口出入参。方向决定语义与可选性规则:
3
+ DTO 描述接口出入参。方向决定可选性规则,由各方向工厂设置;**规则统一只在 `field.optional === undefined` 时生效**(作者已显式设置的跳过):
4
4
 
5
- | 构建器 | 方向 | 用途 |
5
+ | 构建器 | 方向 | 可选性规则 |
6
6
  |---|---|---|
7
- | `buildInput` | input | 新增/修改请求体 |
8
- | `buildOutput` | output | 响应体 |
9
- | `buildQuery` | query | 分页 + 过滤查询(字段恒为可选) |
10
- | `buildPk` | pk | 按主键取详情 |
7
+ | `buildInput` | input | DB 列规则:可空 / 有默认 / 自增列 → 可选;NOT NULL 无默认 → 必填 |
8
+ | `buildOutput` | output | 不设置(保持字段构建器/作者设置) |
9
+ | `buildQuery` | query | 全部可选 |
10
+ | `buildPk` | pk | 主键字段必填,其他字段可选 |
11
11
 
12
12
  ## 从表提取字段
13
13
 
@@ -22,7 +22,7 @@ buildOutput('OrderRow', from(order, [order.fields.id, order.fields.order_no]));
22
22
 
23
23
  // 查询:分页 + 过滤(query 字段恒为可选,.op() 声明比较操作符)
24
24
  buildQuery('OrderPageQuery', {
25
- keyword: dtoField(stringField({ maxLength: 32 })).op('like'),
25
+ keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
26
26
  ...from(order, [order.fields.mer_id]),
27
27
  });
28
28
 
@@ -30,16 +30,23 @@ buildQuery('OrderPageQuery', {
30
30
  buildPk('OrderDetailRequest', from(order, [order.fields.id]));
31
31
  ```
32
32
 
33
- `from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema` 保持指向表。
33
+ `from(table, fields)` 提取表字段包装为 DTO 字段,字段实例与表共享,`name/schema` 保持指向表。**DTO 字段名转 camelCase**(`mer_id` → `merId`),与 DB 列名(snake_case)分离。`from()` 本身不做任何可选性推断——推断在各方向工厂。
34
34
 
35
35
  ## 独立字段
36
36
 
37
- 不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`。
37
+ 不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器,可加 `pattern`、`optional`、`operator`、`default`。
38
38
 
39
39
  ```ts
40
- dtoField(stringField({ maxLength: 32 })).op('like')
40
+ dtoField(stringField({ maxLength: 32 })).setOperator('like')
41
+ dtoField(intField()).setDefault(0) // TypeBox default 注解
41
42
  ```
42
43
 
44
+ ## 默认值
45
+
46
+ - `setDefault(v)` 设 DTO 层默认值,渲染为 TypeBox `default:` 注解(`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`)。
47
+ - 枚举默认值必须是该枚举的成员值(value),渲染时解析为成员引用;非成员值直接报错。
48
+ - 未显式设置时,fallback 到字段构建器的 `default`(DB 默认值,string),`from()` 提取的字段自动带出。
49
+
43
50
  ## 继承基础 schema
44
51
 
45
52
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.ts",
package/src/dsl.ts CHANGED
@@ -8,17 +8,24 @@ export interface SchemaBase {
8
8
  description?: string;
9
9
  }
10
10
 
11
+ /** Field collection schemas (DB table vs DTO message); `type` is the discriminator */
12
+ export interface CollectionSchemaBase extends SchemaBase {
13
+ type: string;
14
+ }
15
+
11
16
  export interface BaseField {
12
17
  name: string;
13
18
  /** 显示名称(中文标签) */
14
19
  label?: string;
15
20
  /** 字段描述 */
16
21
  description?: string;
22
+ /** 业务语义码(如 'merchant_name'),驱动 mock 生成等下游消费 */
23
+ semantic?: string;
17
24
  optional?: boolean;
18
25
  readOnly?: boolean;
19
26
  default?: string;
20
27
  /** 所属 schema(db 或 dto) */
21
- schema?: SchemaBase;
28
+ schema?: CollectionSchemaBase;
22
29
  }
23
30
 
24
31
  interface StringField extends BaseField {
@@ -132,7 +139,24 @@ export type ForeignKey = {
132
139
  references: Field | Field[];
133
140
  };
134
141
 
135
- export interface TableSchema extends SchemaBase {
142
+ export interface TableSchemaOptions {
143
+ description?: string;
144
+ paginated?: boolean;
145
+ actor?: boolean;
146
+ generator?: string;
147
+ autoIncrement?: Field;
148
+ primaryKey?: Field | Field[];
149
+ indexes?: Index[];
150
+ foreignKeys?: Record<string, ForeignKey>;
151
+ /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
152
+ phrase?: DictionaryEntry;
153
+ fields: Record<string, Field>;
154
+ }
155
+
156
+ export class TableSchema implements CollectionSchemaBase {
157
+ type = 'table';
158
+ name: string;
159
+ description?: string;
136
160
  /** 分页 */
137
161
  paginated?: boolean;
138
162
  /** 系统操作者(如小程序为 C 端用户,管理端为运营) */
@@ -148,6 +172,28 @@ export interface TableSchema extends SchemaBase {
148
172
  /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
149
173
  phrase?: DictionaryEntry;
150
174
  fields: Record<string, Field>;
175
+
176
+ constructor(name: string, options: TableSchemaOptions) {
177
+ this.name = name;
178
+ this.description = options.description;
179
+ this.paginated = options.paginated;
180
+ this.actor = options.actor;
181
+ this.generator = options.generator;
182
+ this.autoIncrement = options.autoIncrement;
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
+ }
151
197
  }
152
198
 
153
199
  // Field builders: type and jsType are fixed, pass extra properties only.
@@ -200,23 +246,8 @@ export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): E
200
246
  return { name: '', type: 'enum', jsType, ...extra };
201
247
  }
202
248
 
203
- export function defineTable(
204
- name: string,
205
- schema: {
206
- description?: string;
207
- paginated?: boolean;
208
- actor?: boolean;
209
- generator?: string;
210
- autoIncrement?: Field;
211
- primaryKey?: Field | Field[];
212
- indexes?: Index[];
213
- foreignKeys?: Record<string, ForeignKey>;
214
- /** 引用的实体短语(词典条目) */
215
- phrase?: DictionaryEntry;
216
- fields: Record<string, Field>;
217
- },
218
- ): TableSchema {
219
- const table: TableSchema = { name, ...schema };
249
+ export function defineTable(name: string, schema: TableSchemaOptions): TableSchema {
250
+ const table = new TableSchema(name, schema);
220
251
  for (const key of Object.keys(table.fields)) {
221
252
  const field = table.fields[key];
222
253
  if (field.schema && field.schema !== table) {
package/src/dto.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { BaseField, Field, SchemaBase, TableSchema } from './dsl';
1
+ import { BaseField, CollectionSchemaBase, Field, SchemaBase, TableSchema } from './dsl';
2
+ import { toCamelCase } from './utils';
2
3
 
3
4
  // Interface (DTO) field definitions.
4
5
  // Naming convention: all DTO types and builders use the Dto prefix.
@@ -16,18 +17,15 @@ export interface ImportRef {
16
17
  from: string;
17
18
  /** Named export, e.g. 'PageRequest' */
18
19
  name: string;
19
- /** Generic type arguments for the base schema (same-file DTO export names), e.g. PageResult(AdvertRow) */
20
- args?: string[];
20
+ /**
21
+ * Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
22
+ * Two forms, both local DTOs:
23
+ * string — the DTO export name
24
+ * DtoMessage — the DTO instance itself; the driver resolves it to its name
25
+ */
26
+ args?: (string | DtoMessage)[];
21
27
  }
22
28
 
23
- export type DtoExtras = {
24
- pattern?: string;
25
- /** 联合判断是否可选,定义后优先级高于 field.optional */
26
- optional?: boolean;
27
- /** 查询比较操作符(query 方向字段) */
28
- operator?: Operator;
29
- };
30
-
31
29
  export type DtoArrayFieldDef = BaseField & {
32
30
  type: 'array';
33
31
  jsType: 'array';
@@ -40,23 +38,24 @@ export type DtoObjectFieldDef = BaseField & {
40
38
  properties: Record<string, DtoField>;
41
39
  };
42
40
 
43
- export class DtoField {
41
+ export class DtoField implements SchemaBase {
44
42
  /** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
45
43
  * 构造时未知,由 buildMessage 从 map key 反写。 */
46
44
  name: string;
45
+ /** 字段描述 */
46
+ description?: string;
47
47
  /** 所属 DTO 容器(buildMessage 反写) */
48
48
  schema?: DtoMessage;
49
49
  field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
50
50
  pattern?: string;
51
51
  optional?: boolean;
52
52
  operator?: Operator;
53
+ /** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
54
+ default?: unknown;
53
55
 
54
- constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef, extra: DtoExtras = {}) {
56
+ constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
55
57
  this.name = '';
56
58
  this.field = field;
57
- this.pattern = extra.pattern;
58
- this.optional = extra.optional;
59
- this.operator = extra.operator;
60
59
  }
61
60
 
62
61
  setPattern(value: string): this {
@@ -64,15 +63,29 @@ export class DtoField {
64
63
  return this;
65
64
  }
66
65
 
66
+ setDescription(value: string): this {
67
+ this.description = value;
68
+ return this;
69
+ }
70
+
71
+ getDescription(): string | undefined {
72
+ return this.description;
73
+ }
74
+
67
75
  setOptional(value: boolean): this {
68
76
  this.optional = value;
69
77
  return this;
70
78
  }
71
79
 
80
+ /** Set a default value — emitted as a TypeBox schema default annotation */
81
+ setDefault(value: unknown): this {
82
+ this.default = value;
83
+ return this;
84
+ }
85
+
72
86
  /** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
73
- op(value: Operator): this {
87
+ setOperator(value: Operator): this {
74
88
  this.operator = value;
75
- this.optional = true;
76
89
  return this;
77
90
  }
78
91
 
@@ -99,46 +112,55 @@ export class DtoObjectField extends DtoField {
99
112
  }
100
113
  }
101
114
 
102
- export type DtoDirection = 'input' | 'output' | 'query' | 'pk';
115
+ export enum DtoDirection {
116
+ Input = 'input',
117
+ Output = 'output',
118
+ Query = 'query',
119
+ Pk = 'pk',
120
+ }
103
121
 
104
- export interface DtoMessage extends SchemaBase {
122
+ export class DtoMessage implements CollectionSchemaBase {
123
+ type = 'dto';
124
+ name: string;
125
+ description?: string;
105
126
  /** 方向:输入或输出 */
106
127
  direction: DtoDirection;
107
128
  fields: Record<string, DtoField>;
108
129
  /** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
109
- bases?: ImportRef[];
130
+ bases: ImportRef[] = [];
131
+
132
+ constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
133
+ this.name = name;
134
+ this.direction = direction;
135
+ this.fields = fields;
136
+ this.description = description;
137
+ }
138
+
110
139
  /** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
111
- include(...refs: ImportRef[]): DtoMessage;
140
+ include(...refs: ImportRef[]): this {
141
+ this.bases.push(...refs);
142
+ return this;
143
+ }
112
144
  }
113
145
 
114
- export function dtoField(field: Field, extra: DtoExtras = {}): DtoField {
115
- return new DtoField(field, extra);
146
+ export function dtoField(field: Field): DtoField {
147
+ return new DtoField(field);
116
148
  }
117
149
 
118
- export function dtoArrayField(def: { items: DtoField } & Omit<BaseField, 'name'>, extra: DtoExtras = {}): DtoArrayField {
119
- return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def }, extra);
150
+ export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
151
+ // Reuse an existing DTO as the array element: expand its fields into an object.
152
+ const items = def.items instanceof DtoMessage
153
+ ? new DtoObjectField({ name: '', type: 'object', jsType: 'object', properties: def.items.fields })
154
+ : def.items;
155
+ return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def, items });
120
156
  }
121
157
 
122
- export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>, extra: DtoExtras = {}): DtoObjectField {
123
- return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def }, extra);
158
+ export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
159
+ return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
124
160
  }
125
161
 
126
162
  function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
127
- const message: DtoMessage = {
128
- name,
129
- direction,
130
- description,
131
- fields,
132
- bases: [],
133
- include(...refs: ImportRef[]): DtoMessage {
134
- this.bases!.push(...refs);
135
- return this;
136
- },
137
- };
138
- if (direction === 'query') {
139
- // Rule B: query/search fields are always optional.
140
- for (const field of Object.values(message.fields)) field.optional = true;
141
- }
163
+ const message = new DtoMessage(name, direction, fields, description);
142
164
  // Write back the DTO field name from the map key (safe: DtoField instances
143
165
  // are created per DTO, never shared).
144
166
  for (const key of Object.keys(message.fields)) {
@@ -157,19 +179,40 @@ function buildMessage(name: string, direction: DtoDirection, fields: Record<stri
157
179
  }
158
180
 
159
181
  export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
160
- return buildMessage(name, 'input', fields, description);
182
+ const message = buildMessage(name, DtoDirection.Input, fields, description);
183
+ // Rule A — set optionality from the DB column rule (skips fields the author
184
+ // already set): nullable / default / auto-increment → optional, else required.
185
+ for (const field of Object.values(message.fields)) {
186
+ if (field.optional !== undefined) continue;
187
+ const f = field.field as Field;
188
+ if (f.schema?.type !== 'table') continue;
189
+ const table = f.schema as TableSchema;
190
+ field.optional = f.optional !== false || f.default !== undefined || table.autoIncrement === f;
191
+ }
192
+ return message;
161
193
  }
162
194
 
163
195
  export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
164
- return buildMessage(name, 'output', fields, description);
196
+ return buildMessage(name, DtoDirection.Output, fields, description);
165
197
  }
166
198
 
167
199
  export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
168
- return buildMessage(name, 'query', fields, description);
200
+ const message = buildMessage(name, DtoDirection.Query, fields, description);
201
+ // Rule B: query/search fields are always optional.
202
+ for (const field of Object.values(message.fields)) field.optional = true;
203
+ return message;
169
204
  }
170
205
 
171
206
  export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
172
- return buildMessage(name, 'pk', fields, description);
207
+ const message = buildMessage(name, DtoDirection.Pk, fields, description);
208
+ // Rule P: PK locator fields are required, other fields are optional.
209
+ for (const field of Object.values(message.fields)) {
210
+ if (field.optional !== undefined) continue;
211
+ const f = field.field as Field;
212
+ const table = f.schema as TableSchema | undefined;
213
+ field.optional = table !== undefined && table.isPk(f) ? false : true;
214
+ }
215
+ return message;
173
216
  }
174
217
 
175
218
  /** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
@@ -179,7 +222,9 @@ export function from(table: TableSchema, fields: Field[]): Record<string, DtoFie
179
222
  if (field.schema !== table) {
180
223
  throw new Error(`dto.from(${table.name}): field ${field.name} does not belong to this table`);
181
224
  }
182
- out[field.name] = dtoField(field);
225
+ // DTO field name is camelCase (mer_id → merId); the underlying field.name
226
+ // stays snake_case (DB column).
227
+ out[toCamelCase(field.name)] = dtoField(field);
183
228
  }
184
229
  return out;
185
230
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './dsl';
2
2
  export * from './dto';
3
+ export * from './utils';
3
4
  export * from './project';
4
5
  export * from './prototype';
5
6
  export * from './dictionary';
@@ -53,9 +53,12 @@ export function renderPageFlowMermaid(schema: PageFlow): string {
53
53
 
54
54
  const byApp = new Map<string, Page[]>();
55
55
  for (const p of schema.pages) {
56
- const list = byApp.get(p.app.name) ?? [];
57
- list.push(p);
58
- byApp.set(p.app.name, list);
56
+ const list = byApp.get(p.app.name);
57
+ if (!list) {
58
+ byApp.set(p.app.name, [p]);
59
+ } else {
60
+ list.push(p);
61
+ }
59
62
  }
60
63
 
61
64
  let appIdx = 0;