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.
- package/dist/befly.js +184 -101
- package/dist/befly.min.js +18 -18
- package/dist/checks/checkTable.js +76 -62
- package/dist/lib/validator.js +12 -10
- package/dist/sync/syncTable.d.ts +1 -1
- package/dist/sync/syncTable.js +34 -7
- package/dist/types/validate.d.ts +1 -1
- package/dist/utils/dbFieldRules.d.ts +31 -0
- package/dist/utils/dbFieldRules.js +94 -0
- package/dist/utils/normalizeFieldDefinition.js +3 -33
- package/package.json +2 -2
|
@@ -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
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const
|
|
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 (!
|
|
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" && !
|
|
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 (!
|
|
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 ||
|
|
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 (
|
|
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} 为
|
|
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} 为
|
|
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} 为
|
|
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} 为
|
|
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 (
|
|
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)})`);
|
package/dist/lib/validator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
261
|
-
|
|
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
|
-
|
|
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();
|
package/dist/sync/syncTable.d.ts
CHANGED
|
@@ -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;
|
package/dist/sync/syncTable.js
CHANGED
|
@@ -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
|
|
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
|
|
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() !== ""
|
package/dist/types/validate.d.ts
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
|
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.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.16.1",
|
|
4
|
+
"gitHead": "effac3f749af5f1395048a1ed528eac7e55eaed5",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|