befly 3.15.27 → 3.16.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.
@@ -1,5 +1,6 @@
1
1
  import { Logger } from "../lib/logger";
2
2
  import { CoreError } from "../types/coreError";
3
+ import { isDecimalDbType as isDecimalDbTypeRule, isEnumDbType as isEnumDbTypeRule, isFloatDbType as isFloatDbTypeRule, isIntDbType as isIntDbTypeRule, isJsonDbType as isJsonDbTypeRule, isNumericDbType as isNumericDbTypeRule, isStringDbType as isStringDbTypeRule, isTextDbType as isTextDbTypeRule, normalizeTypeAndInput, parseEnumRuleValues } from "../utils/dbFieldRules";
3
4
  import { formatValuePreview, isAliasInput, isEnumInput, isJsonValue, isRegexInput, snakeCase } from "../utils/util";
4
5
  /**
5
6
  * 保留字段列表
@@ -9,54 +10,13 @@ const RESERVED_FIELD_SET = new Set(RESERVED_FIELDS);
9
10
  /**
10
11
  * 允许的数据库字段类型
11
12
  */
12
- const FIELD_TYPES = ["tinyint", "smallint", "mediumint", "int", "bigint", "decimal", "float", "double", "char", "varchar", "tinytext", "text", "mediumtext", "longtext", "datetime", "json"];
13
+ const FIELD_TYPES = ["tinyint", "smallint", "mediumint", "int", "bigint", "decimal", "float", "double", "char", "varchar", "enum", "tinytext", "text", "mediumtext", "longtext", "datetime", "timestamp", "json"];
13
14
  const FIELD_TYPE_SET = new Set(FIELD_TYPES);
14
15
  /**
15
16
  * 允许的输入类型
16
17
  */
17
18
  const INPUT_TYPES = ["number", "integer", "string", "char", "array", "array_number", "array_integer", "json", "json_number", "json_integer"];
18
19
  const INPUT_TYPE_SET = new Set(INPUT_TYPES);
19
- /**
20
- * 根据数据库类型推导默认 input 类型。
21
- */
22
- function inferInputByType(dbType) {
23
- switch (dbType.toLowerCase()) {
24
- case "tinyint":
25
- case "smallint":
26
- case "mediumint":
27
- case "int":
28
- case "bigint":
29
- return "integer";
30
- case "decimal":
31
- case "float":
32
- case "double":
33
- return "number";
34
- case "char":
35
- case "varchar":
36
- case "tinytext":
37
- case "text":
38
- case "mediumtext":
39
- case "longtext":
40
- return "string";
41
- case "datetime":
42
- return "string";
43
- case "json":
44
- return "json";
45
- default:
46
- return "string";
47
- }
48
- }
49
- /**
50
- * 归一化 type 与 input,input 为空时使用推导值。
51
- */
52
- function normalizeTypeAndInput(type, input) {
53
- const rawType = String(type || "").trim();
54
- const rawInput = String(input || "").trim();
55
- return {
56
- type: rawType,
57
- input: rawInput || inferInputByType(rawType)
58
- };
59
- }
60
20
  /**
61
21
  * 允许的字段属性列表
62
22
  */
@@ -220,21 +180,32 @@ export async function checkTable(tables, config) {
220
180
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 input "${normalizedInput}" 不合法,` + `必须为${INPUT_TYPES.join("、")}之一,或正则/枚举/正则别名`);
221
181
  hasError = true;
222
182
  }
223
- const isStringDbType = ["char", "varchar", "tinytext", "text", "mediumtext", "longtext"].includes(effectiveType);
224
- const isTextDbType = ["tinytext", "text", "mediumtext", "longtext"].includes(effectiveType);
225
- const isIntDbType = ["tinyint", "smallint", "mediumint", "int", "bigint"].includes(effectiveType);
226
- const isDecimalDbType = effectiveType === "decimal";
227
- const isFloatDbType = effectiveType === "float" || effectiveType === "double";
228
- const isJsonDbType = effectiveType === "json";
229
- const isNumericDbType = isIntDbType || isDecimalDbType || isFloatDbType;
183
+ const isDecimalDbType = isDecimalDbTypeRule(effectiveType);
184
+ const isFloatDbType = isFloatDbTypeRule(effectiveType);
185
+ const isJsonDbType = isJsonDbTypeRule(effectiveType);
186
+ const isEnumDbType = isEnumDbTypeRule(effectiveType);
187
+ const isNumericDbType = isNumericDbTypeRule(effectiveType);
188
+ const isStringDbTypeNormalized = isStringDbTypeRule(effectiveType);
189
+ const isTextDbTypeNormalized = isTextDbTypeRule(effectiveType);
190
+ const isIntDbTypeNormalized = isIntDbTypeRule(effectiveType);
191
+ if (isEnumDbType) {
192
+ if (normalizedInput.trim() === "") {
193
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,必须设置 input 枚举规则(例如 a|b|c)`);
194
+ hasError = true;
195
+ }
196
+ else if (!isEnumInput(normalizedInput)) {
197
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,input 必须为枚举规则(例如 a|b|c),当前为 ${formatValuePreview(normalizedInput)}`);
198
+ hasError = true;
199
+ }
200
+ }
230
201
  if (isRegexInput(normalizedInput) || isEnumInput(normalizedInput) || isAliasInput(normalizedInput)) {
231
- if (!isStringDbType) {
232
- Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 input 使用正则/枚举/正则别名,仅允许字符串类字段(char/varchar/text)`);
202
+ if (!isStringDbTypeNormalized) {
203
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 input 使用正则/枚举/正则别名,仅允许字符串类字段(char/varchar/enum/text)`);
233
204
  hasError = true;
234
205
  }
235
206
  }
236
207
  if (normalizedInput === "number" || normalizedInput === "integer") {
237
- if (normalizedInput === "integer" && !isIntDbType) {
208
+ if (normalizedInput === "integer" && !isIntDbTypeNormalized) {
238
209
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 input=${normalizedInput} 仅允许整数类字段(tinyint/smallint/mediumint/int/bigint)`);
239
210
  hasError = true;
240
211
  }
@@ -244,13 +215,13 @@ export async function checkTable(tables, config) {
244
215
  }
245
216
  }
246
217
  if (normalizedInput === "array" || normalizedInput === "array_number" || normalizedInput === "array_integer") {
247
- if (!isStringDbType) {
218
+ if (!isStringDbTypeNormalized) {
248
219
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 input=${normalizedInput} 仅允许字符串类字段(char/varchar/text)`);
249
220
  hasError = true;
250
221
  }
251
222
  }
252
223
  if (normalizedInput === "json" || normalizedInput === "json_number" || normalizedInput === "json_integer") {
253
- if (!(isJsonDbType || isTextDbType)) {
224
+ if (!(isJsonDbType || isTextDbTypeNormalized)) {
254
225
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 input=${normalizedInput} 仅允许 json 或 text 类字段`);
255
226
  hasError = true;
256
227
  }
@@ -273,7 +244,7 @@ export async function checkTable(tables, config) {
273
244
  }
274
245
  }
275
246
  // 类型联动校验 + 默认值规则
276
- if (isTextDbType || isJsonDbType) {
247
+ if (isTextDbTypeNormalized || isJsonDbType) {
277
248
  // text/json:min/max 必须为 null,默认值必须为 null,且不支持索引/唯一约束
278
249
  if (field.min !== undefined && field.min !== null) {
279
250
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 的 ${effectiveType} 类型最小值应为 null,当前为 "${field.min}"`);
@@ -296,25 +267,68 @@ export async function checkTable(tables, config) {
296
267
  hasError = true;
297
268
  }
298
269
  }
299
- else if (effectiveType === "datetime") {
300
- // datetime:对应 MySQL DATETIME(到秒)
270
+ else if (effectiveType === "datetime" || effectiveType === "timestamp") {
271
+ // datetime/timestamp:对应 MySQL DATETIME/TIMESTAMP(到秒)
301
272
  // - min/max 必须为 null
302
273
  // - default 必须为 null(避免把 DDL 表达式当作运行期默认值)
303
274
  // - 不允许 unsigned
304
275
  if (field.min !== undefined && field.min !== null) {
305
- Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 datetime 类型,min 必须为 null,当前为 "${field.min}"`);
276
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${effectiveType} 类型,min 必须为 null,当前为 "${field.min}"`);
306
277
  hasError = true;
307
278
  }
308
279
  if (field.max !== undefined && field.max !== null) {
309
- Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 datetime 类型,max 必须为 null,当前为 "${field.max}"`);
280
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${effectiveType} 类型,max 必须为 null,当前为 "${field.max}"`);
310
281
  hasError = true;
311
282
  }
312
283
  if (field.default !== undefined && field.default !== null) {
313
- Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 datetime 类型,默认值必须为 null(如需当前时间,请在业务写入时赋值)。当前为 ${formatValuePreview(field.default)}`);
284
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${effectiveType} 类型,默认值必须为 null(如需当前时间,请在业务写入时赋值)。当前为 ${formatValuePreview(field.default)}`);
285
+ hasError = true;
286
+ }
287
+ if (field.unsigned !== undefined) {
288
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${effectiveType} 类型,不允许设置 unsigned`);
289
+ hasError = true;
290
+ }
291
+ }
292
+ else if (effectiveType === "enum") {
293
+ // enum:对应 MySQL ENUM('a','b',...)。
294
+ // 约束:必须提供 input 枚举规则(a|b|c),由上方 isEnumDbType 分支保证。
295
+ // - min/max/precision/scale 必须为 null
296
+ // - default 若提供,必须为枚举值之一
297
+ // - 若 nullable!=true,则必须提供 default(避免 NOT NULL 无默认值导致写入失败)
298
+ // - 不允许 unsigned
299
+ if (field.min !== undefined && field.min !== null) {
300
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,min 必须为 null,当前为 "${field.min}"`);
301
+ hasError = true;
302
+ }
303
+ if (field.max !== undefined && field.max !== null) {
304
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,max 必须为 null,当前为 "${field.max}"`);
305
+ hasError = true;
306
+ }
307
+ if (field.precision !== undefined && field.precision !== null) {
308
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,不允许设置 precision`);
309
+ hasError = true;
310
+ }
311
+ if (field.scale !== undefined && field.scale !== null) {
312
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,不允许设置 scale`);
314
313
  hasError = true;
315
314
  }
316
315
  if (field.unsigned !== undefined) {
317
- Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 datetime 类型,不允许设置 unsigned`);
316
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,不允许设置 unsigned`);
317
+ hasError = true;
318
+ }
319
+ const enumValues = parseEnumRuleValues(normalizedInput);
320
+ if (field.default === undefined || field.default === null) {
321
+ if (field.nullable !== true) {
322
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,nullable!=true 时必须设置 default(且 default 必须为枚举值之一)`);
323
+ hasError = true;
324
+ }
325
+ }
326
+ else if (typeof field.default !== "string") {
327
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,默认值必须为字符串或 null` + `(typeof=${typeof field.default},value=${formatValuePreview(field.default)})`);
328
+ hasError = true;
329
+ }
330
+ else if (!enumValues.includes(field.default)) {
331
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 enum 类型,默认值不在枚举范围内(default=${formatValuePreview(field.default)},enums=${formatValuePreview(normalizedInput)})`);
318
332
  hasError = true;
319
333
  }
320
334
  }
@@ -382,7 +396,7 @@ export async function checkTable(tables, config) {
382
396
  hasError = true;
383
397
  }
384
398
  }
385
- else if (isIntDbType) {
399
+ else if (isIntDbTypeNormalized) {
386
400
  // 整数类型:default 如果存在,必须为 null 或 number
387
401
  if (field.default !== undefined && field.default !== null && typeof field.default !== "number") {
388
402
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${effectiveType} 类型,默认值必须为数字或 null` + `(typeof=${typeof field.default},value=${formatValuePreview(field.default)})`);
@@ -3,6 +3,7 @@
3
3
  * 纯静态类设计,简洁易用
4
4
  */
5
5
  import { RegexAliases, getCompiledRegex } from "../configs/presetRegexp";
6
+ import { parseEnumRuleValues } from "../utils/dbFieldRules";
6
7
  import { normalizeFieldDefinition } from "../utils/normalizeFieldDefinition";
7
8
  import { isEnumInput, isFiniteNumber, isIntegerNumber, isJsonObject, isJsonPrimitive, isJsonStructure, isPlainObject, isRegexInput } from "../utils/util";
8
9
  const INPUT_TYPE_SET = new Set(["number", "integer", "string", "char", "array", "array_number", "array_integer", "json", "json_number", "json_integer"]);
@@ -209,13 +210,15 @@ export class Validator {
209
210
  }
210
211
  case "string": {
211
212
  if (typeof value === "string") {
212
- if (String(dbType || "").toLowerCase() === "datetime") {
213
+ const dbTypeNormalized = String(dbType || "").toLowerCase();
214
+ if (dbTypeNormalized === "datetime" || dbTypeNormalized === "timestamp") {
213
215
  const trimmed = value.trim();
214
216
  return { value: trimmed, error: null };
215
217
  }
216
218
  return { value: value, error: null };
217
219
  }
218
- if (String(dbType || "").toLowerCase() === "datetime") {
220
+ const dbTypeNormalized = String(dbType || "").toLowerCase();
221
+ if (dbTypeNormalized === "datetime" || dbTypeNormalized === "timestamp") {
219
222
  return { value: null, error: "必须是时间字符串" };
220
223
  }
221
224
  return { value: null, error: "必须是字符串" };
@@ -257,8 +260,9 @@ export class Validator {
257
260
  return { value: null, error: "必须是JSON对象或数组" };
258
261
  return checkJsonLeaves(value, "integer") ? { value: value, error: null } : { value: null, error: "JSON值必须是整数" };
259
262
  default: {
260
- // 兜底:若 dbType 为 datetime,仍需字符串,且支持 YYYY-MM-DD 补零
261
- if (String(dbType || "").toLowerCase() === "datetime") {
263
+ // 兜底:若 dbType 为 datetime/timestamp,仍需字符串
264
+ const dbTypeNormalized = String(dbType || "").toLowerCase();
265
+ if (dbTypeNormalized === "datetime" || dbTypeNormalized === "timestamp") {
262
266
  if (typeof value !== "string")
263
267
  return { value: null, error: "必须是时间字符串" };
264
268
  const trimmed = value.trim();
@@ -302,10 +306,7 @@ export class Validator {
302
306
  return "格式不正确";
303
307
  }
304
308
  if (isEnum) {
305
- const enums = inputRaw
306
- .split("|")
307
- .map((item) => item.trim())
308
- .filter((item) => item !== "");
309
+ const enums = parseEnumRuleValues(inputRaw);
309
310
  if (!enums.includes(value))
310
311
  return "值不在枚举范围内";
311
312
  }
@@ -322,7 +323,8 @@ export class Validator {
322
323
  if (!isJsonStructure(value))
323
324
  return "必须是JSON对象或数组";
324
325
  }
325
- if (String(type || "").toLowerCase() === "datetime") {
326
+ const normalizedTypeForDb = String(type || "").toLowerCase();
327
+ if (normalizedTypeForDb === "datetime" || normalizedTypeForDb === "timestamp") {
326
328
  if (typeof value !== "string")
327
329
  return "必须是时间字符串";
328
330
  if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value))
@@ -400,7 +402,7 @@ export class Validator {
400
402
  }
401
403
  // 类型默认值(字段未定义 default 时使用)
402
404
  const normalizedType = String(type || "").toLowerCase();
403
- if (normalizedType === "datetime" || normalizedType === "json") {
405
+ if (normalizedType === "datetime" || normalizedType === "timestamp" || normalizedType === "json" || normalizedType === "enum") {
404
406
  return null;
405
407
  }
406
408
  const normalizedInput = String(input || "").toLowerCase();
@@ -69,7 +69,7 @@ export declare class SyncTable {
69
69
  * - 若未来需要支持更复杂的标识符(如关键字/点号/反引号转义),应在此处统一扩展并补测试
70
70
  */
71
71
  static quoteIdentifier(identifier: string): string;
72
- static getSqlType(fieldType: string, fieldMax: number | null, unsigned?: boolean, precision?: number | null, scale?: number | null): string;
72
+ static getSqlType(fieldType: string, fieldMax: number | null, unsigned?: boolean, precision?: number | null, scale?: number | null, enumInput?: string | null): string;
73
73
  static resolveDefaultValue(fieldDefault: any, fieldType: string): any;
74
74
  static generateDefaultSql(actualDefault: any, fieldType: string): string;
75
75
  private static normalizeDefaultForCompare;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Logger } from "../lib/logger";
10
10
  import { CoreError, isCoreError } from "../types/coreError";
11
+ import { parseEnumRuleValues } from "../utils/dbFieldRules";
11
12
  import { normalizeFieldDefinition } from "../utils/normalizeFieldDefinition";
12
13
  import { escapeComment, normalizeColumnDefaultValue } from "../utils/sqlUtil";
13
14
  import { snakeCase } from "../utils/util";
@@ -73,7 +74,9 @@ export class SyncTable {
73
74
  double: "DOUBLE",
74
75
  char: "CHAR",
75
76
  varchar: "VARCHAR",
77
+ enum: "ENUM",
76
78
  datetime: "DATETIME",
79
+ timestamp: "TIMESTAMP",
77
80
  tinytext: "TINYTEXT",
78
81
  text: "MEDIUMTEXT",
79
82
  mediumtext: "MEDIUMTEXT",
@@ -330,9 +333,21 @@ export class SyncTable {
330
333
  }
331
334
  return `\`${trimmed}\``;
332
335
  }
333
- static getSqlType(fieldType, fieldMax, unsigned = false, precision = null, scale = null) {
336
+ static getSqlType(fieldType, fieldMax, unsigned = false, precision = null, scale = null, enumInput = null) {
334
337
  const normalizedType = String(fieldType || "").toLowerCase();
335
338
  const typeMapping = SyncTable.TYPE_MAPPING;
339
+ if (normalizedType === "enum") {
340
+ const values = parseEnumRuleValues(String(enumInput || ""));
341
+ if (values.length === 0) {
342
+ throw new Error(`同步表:内部错误:enum 类型缺失 input 枚举值(应由 checkTable 阻断)`);
343
+ }
344
+ const quoted = [];
345
+ for (const v of values) {
346
+ const escaped = String(v).replace(/'/g, "''");
347
+ quoted.push(`'${escaped}'`);
348
+ }
349
+ return `${typeMapping[normalizedType]}(${quoted.join(",")})`;
350
+ }
336
351
  if (SyncTable.isStringOrArrayType(normalizedType)) {
337
352
  // 约束由 checkTable 统一保证;这里仅做最小断言,避免生成明显无效的 SQL。
338
353
  if (typeof fieldMax !== "number") {
@@ -371,9 +386,15 @@ export class SyncTable {
371
386
  case "char":
372
387
  case "varchar":
373
388
  return "";
389
+ case "enum":
390
+ // enum:默认不自动生成 DEFAULT(由表定义/业务决定),用 "null" sentinel 表示不生成。
391
+ return "null";
374
392
  case "datetime":
375
393
  // datetime 默认不生成 DEFAULT;用 "null" 作为 sentinel(与 TEXT 类似)
376
394
  return "null";
395
+ case "timestamp":
396
+ // timestamp 默认不生成 DEFAULT;用 "null" 作为 sentinel(与 datetime 一致)
397
+ return "null";
377
398
  case "tinytext":
378
399
  case "text":
379
400
  case "mediumtext":
@@ -389,6 +410,10 @@ export class SyncTable {
389
410
  if (SyncTable.TEXT_FAMILY.has(normalizedType) || normalizedType === "json" || actualDefault === "null") {
390
411
  return "";
391
412
  }
413
+ if (normalizedType === "enum") {
414
+ const escaped = String(actualDefault).replace(/'/g, "''");
415
+ return ` DEFAULT '${escaped}'`;
416
+ }
392
417
  if (SyncTable.INT_TYPES.has(normalizedType) || SyncTable.DECIMAL_TYPES.has(normalizedType) || SyncTable.FLOAT_TYPES.has(normalizedType) || SyncTable.isStringOrArrayType(normalizedType)) {
393
418
  if (typeof actualDefault === "number" && !Number.isNaN(actualDefault)) {
394
419
  return ` DEFAULT ${actualDefault}`;
@@ -398,8 +423,8 @@ export class SyncTable {
398
423
  return ` DEFAULT '${escaped}'`;
399
424
  }
400
425
  }
401
- if (normalizedType === "datetime") {
402
- // datetime:默认值仅支持字符串(到秒)或 CURRENT_TIMESTAMP 表达式。
426
+ if (normalizedType === "datetime" || normalizedType === "timestamp") {
427
+ // datetime/timestamp:默认值仅支持字符串(到秒)或 CURRENT_TIMESTAMP 表达式。
403
428
  // 注意:checkTable 默认要求 default 为 null;这里仍做 DDL 生成兜底,便于测试/内部调用。
404
429
  if (typeof actualDefault === "string") {
405
430
  const trimmed = actualDefault.trim();
@@ -500,7 +525,7 @@ export class SyncTable {
500
525
  const normalized = normalizeFieldDefinition(fieldDef);
501
526
  const dbFieldName = snakeCase(fieldKey);
502
527
  const colQuoted = SyncTable.quoteIdentifier(dbFieldName);
503
- const sqlType = SyncTable.getSqlType(normalized.type, normalized.max, normalized.unsigned, normalized.precision, normalized.scale);
528
+ const sqlType = SyncTable.getSqlType(normalized.type, normalized.max, normalized.unsigned, normalized.precision, normalized.scale, normalized.input);
504
529
  const actualDefault = SyncTable.resolveDefaultValue(normalized.default, normalized.type);
505
530
  const defaultSql = SyncTable.generateDefaultSql(actualDefault, normalized.type);
506
531
  const uniqueSql = normalized.unique ? " UNIQUE" : "";
@@ -570,7 +595,8 @@ export class SyncTable {
570
595
  // 统一由 run() 收集所有不兼容项后再一次性抛错。
571
596
  const typeChange = comparison.find((c) => c.type === "datatype");
572
597
  if (typeChange) {
573
- const expectedType = SyncTable.getSqlType(fieldDef.type, fieldDef.max ?? null, fieldDef.unsigned ?? false, fieldDef.precision ?? null, fieldDef.scale ?? null);
598
+ const expectedNormalized = normalizeFieldDefinition(fieldDef);
599
+ const expectedType = SyncTable.getSqlType(expectedNormalized.type, expectedNormalized.max, expectedNormalized.unsigned, expectedNormalized.precision, expectedNormalized.scale, expectedNormalized.input);
574
600
  const incompatible = SyncTable.getIncompatibleTypeChange(options.tableName, dbFieldName, String(typeChange.current ?? ""), expectedType);
575
601
  if (incompatible) {
576
602
  incompatibleTypeChanges.push(incompatible);
@@ -657,7 +683,8 @@ export class SyncTable {
657
683
  const typeChange = comparison.find((c) => c.type === "datatype");
658
684
  if (!typeChange)
659
685
  continue;
660
- const expectedType = SyncTable.getSqlType(fieldDef.type, fieldDef.max ?? null, fieldDef.unsigned ?? false, fieldDef.precision ?? null, fieldDef.scale ?? null);
686
+ const expectedNormalized = normalizeFieldDefinition(fieldDef);
687
+ const expectedType = SyncTable.getSqlType(expectedNormalized.type, expectedNormalized.max, expectedNormalized.unsigned, expectedNormalized.precision, expectedNormalized.scale, expectedNormalized.input);
661
688
  const incompatible = SyncTable.getIncompatibleTypeChange(tableName, dbFieldName, String(typeChange.current ?? ""), expectedType);
662
689
  if (incompatible) {
663
690
  out.push(incompatible);
@@ -847,7 +874,7 @@ export class SyncTable {
847
874
  static compareFieldDefinition(existingColumn, fieldDef) {
848
875
  const changes = [];
849
876
  const normalized = normalizeFieldDefinition(fieldDef);
850
- const expectedType = SyncTable.getSqlType(normalized.type, normalized.max, normalized.unsigned, normalized.precision, normalized.scale).toLowerCase().replace(/\s+/g, " ").trim();
877
+ const expectedType = SyncTable.getSqlType(normalized.type, normalized.max, normalized.unsigned, normalized.precision, normalized.scale, normalized.input).toLowerCase().replace(/\s+/g, " ").trim();
851
878
  const currentType = (typeof existingColumn.columnType === "string" && existingColumn.columnType.trim() !== ""
852
879
  ? existingColumn.columnType
853
880
  : typeof existingColumn.type === "string" && existingColumn.type.trim() !== ""
@@ -5,7 +5,7 @@ import type { JsonValue } from "./common";
5
5
  /**
6
6
  * 数据库字段类型
7
7
  */
8
- export type DbFieldType = "tinyint" | "smallint" | "mediumint" | "int" | "bigint" | "decimal" | "float" | "double" | "char" | "varchar" | "tinytext" | "text" | "mediumtext" | "longtext" | "datetime" | "json";
8
+ export type DbFieldType = "tinyint" | "smallint" | "mediumint" | "int" | "bigint" | "decimal" | "float" | "double" | "char" | "varchar" | "enum" | "tinytext" | "text" | "mediumtext" | "longtext" | "datetime" | "timestamp" | "json";
9
9
  /**
10
10
  * 输入参数类型
11
11
  */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 数据库字段规则(供 checkTable / normalizeFieldDefinition / validator / syncTable 复用)
3
+ *
4
+ * 目标:
5
+ * - 避免同一套规则在多个模块重复实现导致漂移
6
+ * - 保持简单直观,不引入过度抽象
7
+ */
8
+ export declare function normalizeDbType(dbType: string): string;
9
+ export declare function isStringDbType(dbType: string): boolean;
10
+ export declare function isTextDbType(dbType: string): boolean;
11
+ export declare function isIntDbType(dbType: string): boolean;
12
+ export declare function isDecimalDbType(dbType: string): boolean;
13
+ export declare function isFloatDbType(dbType: string): boolean;
14
+ export declare function isJsonDbType(dbType: string): boolean;
15
+ export declare function isEnumDbType(dbType: string): boolean;
16
+ export declare function isNumericDbType(dbType: string): boolean;
17
+ /**
18
+ * 根据数据库类型推导默认 input 类型。
19
+ */
20
+ export declare function inferInputByDbType(dbType: string): string;
21
+ /**
22
+ * 归一化 type 与 input,input 为空时使用推导值。
23
+ */
24
+ export declare function normalizeTypeAndInput(type: string, input: string): {
25
+ type: string;
26
+ input: string;
27
+ };
28
+ /**
29
+ * 解析枚举规则(a|b|c)为去重后的值列表(保持顺序)。
30
+ */
31
+ export declare function parseEnumRuleValues(enumInput: string): string[];
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 数据库字段规则(供 checkTable / normalizeFieldDefinition / validator / syncTable 复用)
3
+ *
4
+ * 目标:
5
+ * - 避免同一套规则在多个模块重复实现导致漂移
6
+ * - 保持简单直观,不引入过度抽象
7
+ */
8
+ const STRING_DB_TYPES = new Set(["char", "varchar", "enum", "tinytext", "text", "mediumtext", "longtext"]);
9
+ const TEXT_DB_TYPES = new Set(["tinytext", "text", "mediumtext", "longtext"]);
10
+ const INT_DB_TYPES = new Set(["tinyint", "smallint", "mediumint", "int", "bigint"]);
11
+ const DECIMAL_DB_TYPES = new Set(["decimal"]);
12
+ const FLOAT_DB_TYPES = new Set(["float", "double"]);
13
+ const JSON_DB_TYPES = new Set(["json"]);
14
+ const ENUM_DB_TYPES = new Set(["enum"]);
15
+ export function normalizeDbType(dbType) {
16
+ return String(dbType || "")
17
+ .trim()
18
+ .toLowerCase();
19
+ }
20
+ export function isStringDbType(dbType) {
21
+ return STRING_DB_TYPES.has(normalizeDbType(dbType));
22
+ }
23
+ export function isTextDbType(dbType) {
24
+ return TEXT_DB_TYPES.has(normalizeDbType(dbType));
25
+ }
26
+ export function isIntDbType(dbType) {
27
+ return INT_DB_TYPES.has(normalizeDbType(dbType));
28
+ }
29
+ export function isDecimalDbType(dbType) {
30
+ return DECIMAL_DB_TYPES.has(normalizeDbType(dbType));
31
+ }
32
+ export function isFloatDbType(dbType) {
33
+ return FLOAT_DB_TYPES.has(normalizeDbType(dbType));
34
+ }
35
+ export function isJsonDbType(dbType) {
36
+ return JSON_DB_TYPES.has(normalizeDbType(dbType));
37
+ }
38
+ export function isEnumDbType(dbType) {
39
+ return ENUM_DB_TYPES.has(normalizeDbType(dbType));
40
+ }
41
+ export function isNumericDbType(dbType) {
42
+ const normalized = normalizeDbType(dbType);
43
+ return INT_DB_TYPES.has(normalized) || DECIMAL_DB_TYPES.has(normalized) || FLOAT_DB_TYPES.has(normalized);
44
+ }
45
+ /**
46
+ * 根据数据库类型推导默认 input 类型。
47
+ */
48
+ export function inferInputByDbType(dbType) {
49
+ const normalized = normalizeDbType(dbType);
50
+ if (INT_DB_TYPES.has(normalized)) {
51
+ return "integer";
52
+ }
53
+ if (DECIMAL_DB_TYPES.has(normalized) || FLOAT_DB_TYPES.has(normalized)) {
54
+ return "number";
55
+ }
56
+ if (STRING_DB_TYPES.has(normalized) || normalized === "datetime" || normalized === "timestamp") {
57
+ return "string";
58
+ }
59
+ if (normalized === "json") {
60
+ return "json";
61
+ }
62
+ return "string";
63
+ }
64
+ /**
65
+ * 归一化 type 与 input,input 为空时使用推导值。
66
+ */
67
+ export function normalizeTypeAndInput(type, input) {
68
+ const rawType = String(type || "").trim();
69
+ const rawInput = String(input || "").trim();
70
+ return {
71
+ type: rawType,
72
+ input: rawInput || inferInputByDbType(rawType)
73
+ };
74
+ }
75
+ /**
76
+ * 解析枚举规则(a|b|c)为去重后的值列表(保持顺序)。
77
+ */
78
+ export function parseEnumRuleValues(enumInput) {
79
+ const raw = String(enumInput || "").trim();
80
+ if (raw === "") {
81
+ return [];
82
+ }
83
+ const parts = raw
84
+ .split("|")
85
+ .map((x) => x.trim())
86
+ .filter((x) => x !== "");
87
+ const out = [];
88
+ for (const p of parts) {
89
+ if (!out.includes(p)) {
90
+ out.push(p);
91
+ }
92
+ }
93
+ return out;
94
+ }
@@ -1,38 +1,8 @@
1
- function inferInputByType(dbType) {
2
- switch (dbType.toLowerCase()) {
3
- case "tinyint":
4
- case "smallint":
5
- case "mediumint":
6
- case "int":
7
- case "bigint":
8
- return "integer";
9
- case "decimal":
10
- case "float":
11
- case "double":
12
- return "number";
13
- case "char":
14
- case "varchar":
15
- case "tinytext":
16
- case "text":
17
- case "mediumtext":
18
- case "longtext":
19
- return "string";
20
- case "datetime":
21
- return "string";
22
- case "json":
23
- return "json";
24
- default:
25
- return "string";
26
- }
27
- }
28
- function normalizeTypeAndInput(fieldDef) {
1
+ import { normalizeTypeAndInput } from "./dbFieldRules";
2
+ export function normalizeFieldDefinition(fieldDef) {
29
3
  const rawType = String(fieldDef.type ?? "").trim();
30
4
  const rawInput = typeof fieldDef.input === "string" ? fieldDef.input.trim() : "";
31
- const inferredInput = rawInput || inferInputByType(rawType);
32
- return { type: rawType, input: inferredInput };
33
- }
34
- export function normalizeFieldDefinition(fieldDef) {
35
- const typeAndInput = normalizeTypeAndInput(fieldDef);
5
+ const typeAndInput = normalizeTypeAndInput(rawType, rawInput);
36
6
  let normalizedDefault = fieldDef.default ?? null;
37
7
  if (normalizedDefault === null) {
38
8
  if (typeAndInput.input === "array" || typeAndInput.input === "array_number" || typeAndInput.input === "array_integer") {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.15.27",
4
- "gitHead": "de207dd938013c1c5a13cff065439762162596b2",
3
+ "version": "3.16.1",
4
+ "gitHead": "effac3f749af5f1395048a1ed528eac7e55eaed5",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
7
7
  "keywords": [