@maiyunnet/kebab 9.15.0 → 9.15.2

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/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.15.0";
8
+ export declare const VER = "9.15.2";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.15.0';
9
+ export const VER = '9.15.2';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/lib/db/conn.js CHANGED
@@ -2,6 +2,7 @@ import * as pg from 'pg';
2
2
  import * as lCore from '#kebab/lib/core.js';
3
3
  import * as lTime from '#kebab/lib/time.js';
4
4
  import * as lDb from '#kebab/lib/db.js';
5
+ import * as lSqlValue from '#kebab/lib/sql/value.js';
5
6
  // --- 注册解析器 ---
6
7
  // --- pg 库对以下类型默认返回 string,为与 MySQL 行为保持一致,在此注册解析器转为 JS 原生类型 ---
7
8
  // --- POLYGON: 返回如 ((1,1),(2,2)) 的字符串,解析为 {x, y}[] ---
@@ -155,6 +156,7 @@ export class Connection {
155
156
  'result': 1,
156
157
  };
157
158
  try {
159
+ values = lSqlValue.serializeList(values);
158
160
  this.refreshLast();
159
161
  if (this._lastSql.length === 2) {
160
162
  this._lastSql.splice(0, 1);
@@ -212,6 +214,7 @@ export class Connection {
212
214
  'result': 1,
213
215
  };
214
216
  try {
217
+ values = lSqlValue.serializeList(values);
215
218
  this.refreshLast();
216
219
  if (this._lastSql.length === 2) {
217
220
  this._lastSql.splice(0, 1);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Project: Kebab, User: JianSuoQiYue
3
+ * --- 数据库特殊值的运行时包装与序列化 ---
4
+ */
5
+ import * as kebab from '#kebab/index.js';
6
+ /** --- JSON 数据的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
7
+ declare const jsonValueSymbol: unique symbol;
8
+ /** --- 延迟到数据库边界序列化的 JSON 值 --- */
9
+ export interface IJsonValue<T> {
10
+ readonly [jsonValueSymbol]: true;
11
+ readonly 'value': T;
12
+ }
13
+ /**
14
+ * --- 创建延迟序列化的 JSON 值 ---
15
+ * @param value 原始 JSON 值
16
+ * @returns JSON 包装值
17
+ */
18
+ export declare function json<T>(value: T): IJsonValue<T>;
19
+ /**
20
+ * --- 判断是否为 JSON 包装值 ---
21
+ * @param value 待判断的值
22
+ * @returns 是否为 JSON 包装值
23
+ */
24
+ export declare function isJson(value: unknown): value is IJsonValue<unknown>;
25
+ /**
26
+ * --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
27
+ * @param value 待解包的值
28
+ * @returns 原始值
29
+ */
30
+ export declare function unwrapJson<T>(value: T): T;
31
+ /**
32
+ * --- 将单个数据库参数转换为驱动可识别的值 ---
33
+ * @param value 数据库参数
34
+ * @returns 序列化后的数据库参数
35
+ */
36
+ export declare function serialize(value: kebab.Json): kebab.Json;
37
+ /**
38
+ * --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
39
+ * @param values 数据库参数列表
40
+ * @returns 可直接交给数据库驱动的参数列表
41
+ */
42
+ export declare function serializeList(values?: kebab.DbValue[]): kebab.DbValue[] | undefined;
43
+ export {};
@@ -0,0 +1,59 @@
1
+ import * as lText from '#kebab/lib/text.js';
2
+ /** --- JSON 数据的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
3
+ const jsonValueSymbol = Symbol('kebab.db.json');
4
+ /**
5
+ * --- 创建延迟序列化的 JSON 值 ---
6
+ * @param value 原始 JSON 值
7
+ * @returns JSON 包装值
8
+ */
9
+ export function json(value) {
10
+ return {
11
+ [jsonValueSymbol]: true,
12
+ 'value': value,
13
+ };
14
+ }
15
+ /**
16
+ * --- 判断是否为 JSON 包装值 ---
17
+ * @param value 待判断的值
18
+ * @returns 是否为 JSON 包装值
19
+ */
20
+ export function isJson(value) {
21
+ return value !== null && typeof value === 'object' &&
22
+ !!value[jsonValueSymbol];
23
+ }
24
+ /**
25
+ * --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
26
+ * @param value 待解包的值
27
+ * @returns 原始值
28
+ */
29
+ export function unwrapJson(value) {
30
+ return (isJson(value) ? value.value : value);
31
+ }
32
+ /**
33
+ * --- 将单个数据库参数转换为驱动可识别的值 ---
34
+ * @param value 数据库参数
35
+ * @returns 序列化后的数据库参数
36
+ */
37
+ export function serialize(value) {
38
+ return isJson(value) ? lText.stringifyJson(value.value) : value;
39
+ }
40
+ /**
41
+ * --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
42
+ * @param values 数据库参数列表
43
+ * @returns 可直接交给数据库驱动的参数列表
44
+ */
45
+ export function serializeList(values) {
46
+ if (!values) {
47
+ return undefined;
48
+ }
49
+ let serialized;
50
+ for (let i = 0; i < values.length; ++i) {
51
+ const value = values[i];
52
+ if (!isJson(value)) {
53
+ continue;
54
+ }
55
+ serialized ??= [...values];
56
+ serialized[i] = lText.stringifyJson(value.value);
57
+ }
58
+ return serialized ?? values;
59
+ }
package/lib/fs.js CHANGED
@@ -434,9 +434,12 @@ export async function readToResponse(path, req, res, stat) {
434
434
  // --- 判断缓存以及 MIME 和编码 ---
435
435
  let charset = '';
436
436
  const mimeData = mime.getData(path);
437
- if (['htm', 'html', 'css', 'js', 'mjs', 'xml', 'jpg', 'jpeg', 'svg', 'gif', 'png', 'json'].includes(mimeData.extension)) {
437
+ // --- 文本类文件统一附加 charset,避免浏览器按本地默认编码解析导致乱码 ---
438
+ if (mimeData.mime.startsWith('text/') || ['json', 'xml', 'svg', 'js', 'mjs', 'map', 'webmanifest'].includes(mimeData.extension)) {
438
439
  charset = '; charset=utf-8';
439
- // --- 这些文件可能需要缓存 ---
440
+ }
441
+ // --- 这些文件可能需要缓存 ---
442
+ if (['htm', 'html', 'css', 'js', 'mjs', 'xml', 'jpg', 'jpeg', 'svg', 'gif', 'png', 'json'].includes(mimeData.extension)) {
440
443
  const hash = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
441
444
  const lastModified = stat.mtime.toUTCString();
442
445
  res.setHeader('etag', hash);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Project: Kebab, User: JianSuoQiYue
3
+ * --- SQL 特殊参数的运行时包装与序列化 ---
4
+ */
5
+ import * as kebab from '#kebab/index.js';
6
+ /** --- JSON 参数的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
7
+ declare const jsonValueSymbol: unique symbol;
8
+ /** --- 延迟到数据库边界序列化的 JSON 参数 --- */
9
+ export interface IJsonValue<T> {
10
+ readonly [jsonValueSymbol]: true;
11
+ readonly 'value': T;
12
+ }
13
+ /**
14
+ * --- 创建延迟序列化的 JSON 参数 ---
15
+ * @param value 原始 JSON 值
16
+ * @returns JSON 包装值
17
+ */
18
+ export declare function json<T>(value: T): IJsonValue<T>;
19
+ /**
20
+ * --- 判断是否为 JSON 包装值 ---
21
+ * @param value 待判断的值
22
+ * @returns 是否为 JSON 包装值
23
+ */
24
+ export declare function isJson(value: unknown): value is IJsonValue<unknown>;
25
+ /**
26
+ * --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
27
+ * @param value 待解包的值
28
+ * @returns 原始值
29
+ */
30
+ export declare function unwrapJson<T>(value: T): T;
31
+ /**
32
+ * --- 将单个 SQL 参数转换为数据库驱动可识别的值 ---
33
+ * @param value SQL 参数
34
+ * @returns 序列化后的数据库参数
35
+ */
36
+ export declare function serialize(value: kebab.Json): kebab.Json;
37
+ /**
38
+ * --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
39
+ * @param values SQL 参数列表
40
+ * @returns 可直接交给数据库驱动的参数列表
41
+ */
42
+ export declare function serializeList(values?: kebab.DbValue[]): kebab.DbValue[] | undefined;
43
+ export {};
@@ -0,0 +1,59 @@
1
+ import * as lText from '#kebab/lib/text.js';
2
+ /** --- JSON 参数的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
3
+ const jsonValueSymbol = Symbol('kebab.sql.json');
4
+ /**
5
+ * --- 创建延迟序列化的 JSON 参数 ---
6
+ * @param value 原始 JSON 值
7
+ * @returns JSON 包装值
8
+ */
9
+ export function json(value) {
10
+ return {
11
+ [jsonValueSymbol]: true,
12
+ 'value': value,
13
+ };
14
+ }
15
+ /**
16
+ * --- 判断是否为 JSON 包装值 ---
17
+ * @param value 待判断的值
18
+ * @returns 是否为 JSON 包装值
19
+ */
20
+ export function isJson(value) {
21
+ return value !== null && typeof value === 'object' &&
22
+ !!value[jsonValueSymbol];
23
+ }
24
+ /**
25
+ * --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
26
+ * @param value 待解包的值
27
+ * @returns 原始值
28
+ */
29
+ export function unwrapJson(value) {
30
+ return (isJson(value) ? value.value : value);
31
+ }
32
+ /**
33
+ * --- 将单个 SQL 参数转换为数据库驱动可识别的值 ---
34
+ * @param value SQL 参数
35
+ * @returns 序列化后的数据库参数
36
+ */
37
+ export function serialize(value) {
38
+ return isJson(value) ? lText.stringifyJson(value.value) : value;
39
+ }
40
+ /**
41
+ * --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
42
+ * @param values SQL 参数列表
43
+ * @returns 可直接交给数据库驱动的参数列表
44
+ */
45
+ export function serializeList(values) {
46
+ if (!values) {
47
+ return undefined;
48
+ }
49
+ let serialized;
50
+ for (let i = 0; i < values.length; ++i) {
51
+ const value = values[i];
52
+ if (!isJson(value)) {
53
+ continue;
54
+ }
55
+ serialized ??= [...values];
56
+ serialized[i] = lText.stringifyJson(value.value);
57
+ }
58
+ return serialized ?? values;
59
+ }
package/lib/sql.d.ts CHANGED
@@ -98,7 +98,7 @@ export declare class Sql {
98
98
  /**
99
99
  * --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
100
100
  * --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
101
- * --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
101
+ * --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES (typed nulls), ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
102
102
  * @param table 表名
103
103
  * @param key 用于定位待更新记录的字段名,通常为主键或唯一键,至少必须建立索引;
104
104
  * 该参数是字段名而不是索引名,仅参与 ON / WHERE 匹配,不会被更新
@@ -192,14 +192,13 @@ export declare class Sql {
192
192
  private _whereDataPosition;
193
193
  /**
194
194
  * --- 筛选器 ---
195
- * --- 1. 'city': 'bj', 'type': '2' ---
196
- * --- 2. ['type', '>', '1'] ---
197
- * --- 3. ['type', 'in', ['1', '2']] ---
198
- * --- 4. 'type': ['1', '2'] ---
199
- * --- 5. '$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]], 'type': '2' ---
200
- * --- 6. 'city_in': column('city_out') ---
201
- * --- 7. ['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
202
- * --- 8. ['info', 'json', {'a': 1}] ---
195
+ * --- 标量相等:'city': 'bj', 'type': '2' ---
196
+ * --- 运算符条件:['type', '>', '1'] ---
197
+ * --- 集合条件:['type', 'in', ['1', '2']] 或 'type': ['1', '2'] ---
198
+ * --- 逻辑分组:'$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]] ---
199
+ * --- 字段比较:'city_in': column('city_out') ---
200
+ * --- 原始条件:['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
201
+ * --- JSON 条件:['info', 'json', {'a': 1}] ---
203
202
  * @param s 筛选数据
204
203
  */
205
204
  where(s: string | kebab.Json): this;
@@ -274,11 +273,6 @@ export declare class Sql {
274
273
  private _isValue;
275
274
  /** --- 获取占位符 --- */
276
275
  private _placeholder;
277
- /**
278
- * --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
279
- * @param v 要处理的值
280
- */
281
- private _pgCastSuffix;
282
276
  /**
283
277
  * --- 处理单个值,检测数据类型并返回 SQL 和 data ---
284
278
  * @param v 要处理的值
@@ -326,7 +320,8 @@ export declare function value(val: kebab.DbValue): {
326
320
  'value': kebab.DbValue;
327
321
  };
328
322
  /**
329
- * --- 将对象转换为 JSON 字符串并避开类型检查,用于适配 PostgreSQL 的 jsonb 字段 ---
330
- * @param obj 要转换的 JSON 对象
323
+ * --- 标记需要写入 JSON/jsonb 字段的值;实际序列化延迟到数据库边界 ---
324
+ * @param obj 原始 JSON
325
+ * @returns 类型保持不变的 JSON 包装值
331
326
  */
332
327
  export declare function json<T>(obj: T): T;
package/lib/sql.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as lText from '#kebab/lib/text.js';
2
2
  import * as lCore from '#kebab/lib/core.js';
3
+ import * as lSqlValue from '#kebab/lib/sql/value.js';
3
4
  // --- 第三方 ---
4
5
  import * as mysql2 from 'mysql2/promise';
5
6
  /** --- 服务商定义 --- */
@@ -198,7 +199,7 @@ export class Sql {
198
199
  /**
199
200
  * --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
200
201
  * --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
201
- * --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
202
+ * --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES (typed nulls), ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
202
203
  * @param table 表名
203
204
  * @param key 用于定位待更新记录的字段名,通常为主键或唯一键,至少必须建立索引;
204
205
  * 该参数是字段名而不是索引名,仅参与 ON / WHERE 匹配,不会被更新
@@ -234,17 +235,18 @@ export class Sql {
234
235
  this._sql = [`UPDATE (${selectParts.join(' UNION ALL ')}) AS tmp STRAIGHT_JOIN ${quotedTable} t ON t.${quotedKey} = tmp.${quotedKey} SET ${setClauses}`];
235
236
  }
236
237
  else {
237
- // --- PostgreSQL 使用 UPDATE FROM (VALUES ...) ---
238
+ // --- PostgreSQL 使用 UPDATE FROM (VALUES ...);首行从目标表复合类型取得真实列类型,
239
+ // 避免按 JS 值猜测类型时 uuid、日期、枚举、数组或 NULL 列无法匹配 ---
238
240
  const valueParts = [];
239
- for (let ri = 0; ri < rows.length; ri++) {
240
- const row = rows[ri];
241
+ const typeParts = allCols.map(c => `(NULL::${quotedTable}).${this.field(c)}`);
242
+ valueParts.push(`(${typeParts.join(', ')})`);
243
+ for (const row of rows) {
241
244
  const parts = row.map(v => {
242
245
  const result = this._processValue(v);
243
246
  if (result.data.length > 0) {
244
247
  this._data.push(...result.data);
245
248
  }
246
- // --- 第一行加显式类型转换,帮助 PostgreSQL 推断 VALUES 派生表的列类型 ---
247
- return ri === 0 ? result.sql + this._pgCastSuffix(v) : result.sql;
249
+ return result.sql;
248
250
  });
249
251
  valueParts.push(`(${parts.join(', ')})`);
250
252
  }
@@ -309,16 +311,15 @@ export class Sql {
309
311
  _updateSub(s) {
310
312
  /*
311
313
  [
312
- ['total', '+', '1'], // 1, '1' 可能也是 1 数字类型
314
+ ['total', '+', '1'],
313
315
  {
314
- 'type': '6', // 2
315
- 'type': column('type2'), // 3
316
- // 'type': ['type3'], // 4 - 此写法已被禁止,请用 (3) 代替
317
- 'type': ['(CASE `id` WHEN 1 THEN ? WHEN 2 THEN ? END)', ['val1', 'val2']], // 5
318
- 'point': { 'x': 0, 'y': 0 }, // 6
319
- 'polygon': [ [ { 'x': 0, 'y': 0 }, { ... } ], [ ... ] ], // 7
320
- 'json': { 'a': 1, 'b': { 'c': 2 }, 'c': [ { 'c': 2 } ] }, // 8 - 对象类 json,可能为空对象
321
- 'json2': ['abc'] // 9 - 数组类 json,可能为空数组
316
+ 'type': '6',
317
+ 'type': column('type2'),
318
+ 'type': ['(CASE `id` WHEN 1 THEN ? WHEN 2 THEN ? END)', ['val1', 'val2']],
319
+ 'point': { 'x': 0, 'y': 0 },
320
+ 'polygon': [ [ { 'x': 0, 'y': 0 }, { ... } ], [ ... ] ],
321
+ 'json': json({ 'a': 1, 'b': { 'c': 2 }, 'c': [ { 'c': 2 } ] }),
322
+ 'json2': json(['abc'])
322
323
  }
323
324
  ]
324
325
  */
@@ -327,7 +328,7 @@ export class Sql {
327
328
  for (const k in s) {
328
329
  const v = s[k];
329
330
  if (/^[0-9]+$/.test(k)) {
330
- // --- 1 ---
331
+ // --- 数组运算式:[字段, 运算符, 值] ---
331
332
  const nv = v[2];
332
333
  const isf = this._isField(nv);
333
334
  if (isf) {
@@ -339,17 +340,22 @@ export class Sql {
339
340
  }
340
341
  }
341
342
  else {
343
+ /** --- JSON 标记值需要生成 ::jsonb;普通运算值保持原占位符行为 --- */
344
+ const result = lSqlValue.isJson(nv) ? this._processValue(nv) : {
345
+ 'sql': this._placeholder(),
346
+ 'data': [nv],
347
+ };
342
348
  if (v[1] === '=') {
343
- sql += this.field(v[0]) + ' = ' + this._placeholder() + ', ';
349
+ sql += this.field(v[0]) + ' = ' + result.sql + ', ';
344
350
  }
345
351
  else {
346
- sql += this.field(v[0]) + ' = ' + this.field(v[0]) + ' ' + v[1] + ' ' + this._placeholder() + ', ';
352
+ sql += this.field(v[0]) + ' = ' + this.field(v[0]) + ' ' + v[1] + ' ' + result.sql + ', ';
347
353
  }
348
- this._data.push(nv);
354
+ this._data.push(...result.data);
349
355
  }
350
356
  }
351
357
  else {
352
- // --- 2, 3, 4, 5, 6, 7, 8 ---
358
+ // --- 对象赋值式:{字段: 值} ---
353
359
  sql += this.field(k) + ' = ';
354
360
  const result = this._processValue(v);
355
361
  sql += result.sql + ', ';
@@ -497,14 +503,13 @@ export class Sql {
497
503
  _whereDataPosition = [0, 0];
498
504
  /**
499
505
  * --- 筛选器 ---
500
- * --- 1. 'city': 'bj', 'type': '2' ---
501
- * --- 2. ['type', '>', '1'] ---
502
- * --- 3. ['type', 'in', ['1', '2']] ---
503
- * --- 4. 'type': ['1', '2'] ---
504
- * --- 5. '$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]], 'type': '2' ---
505
- * --- 6. 'city_in': column('city_out') ---
506
- * --- 7. ['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
507
- * --- 8. ['info', 'json', {'a': 1}] ---
506
+ * --- 标量相等:'city': 'bj', 'type': '2' ---
507
+ * --- 运算符条件:['type', '>', '1'] ---
508
+ * --- 集合条件:['type', 'in', ['1', '2']] 或 'type': ['1', '2'] ---
509
+ * --- 逻辑分组:'$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]] ---
510
+ * --- 字段比较:'city_in': column('city_out') ---
511
+ * --- 原始条件:['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
512
+ * --- JSON 条件:['info', 'json', {'a': 1}] ---
508
513
  * @param s 筛选数据
509
514
  */
510
515
  where(s) {
@@ -546,18 +551,18 @@ export class Sql {
546
551
  for (const k in s) {
547
552
  const v = s[k];
548
553
  if (/^[0-9]+$/.test(k)) {
549
- // --- 2, 3, 7 ---
554
+ // --- 数组条件或原始 SQL 条件 ---
550
555
  if (v[2] === undefined) {
551
- // --- 7 ---
556
+ // --- 原始 SQL 条件及参数 ---
552
557
  sql += this.field(v[0]) + ' AND ';
553
558
  if (v[1] !== undefined) {
554
559
  data.push(...v[1]);
555
560
  }
556
561
  }
557
562
  else if (typeof v[1] === 'string' && ['json', 'json_in', 'json_key', 'json_any', 'json_all', 'json_overlaps'].includes(v[1].toLowerCase())) {
558
- // --- json ---
563
+ // --- JSON 操作符条件 ---
559
564
  const op = v[1].toLowerCase();
560
- const nv = v[2];
565
+ const nv = lSqlValue.unwrapJson(v[2]);
561
566
  if (op === 'json') {
562
567
  if (this._service === ESERVICE.MYSQL) {
563
568
  sql += `JSON_CONTAINS(${this.field(v[0])}, ${this._placeholder()}) AND `;
@@ -614,7 +619,7 @@ export class Sql {
614
619
  }
615
620
  }
616
621
  else if (v[2] === null) {
617
- // --- 3: null ---
622
+ // --- NULL 比较 ---
618
623
  let opera = v[1];
619
624
  if (opera === '!=' || opera === '!==' || opera === '<>') {
620
625
  opera = 'IS NOT';
@@ -628,7 +633,7 @@ export class Sql {
628
633
  sql += this.field(v[0]) + ' ' + opera + ' NULL AND ';
629
634
  }
630
635
  else if (Array.isArray(v[2])) {
631
- // --- 3 ---
636
+ // --- IN 等集合比较 ---
632
637
  sql += this.field(v[0]) + ' ' + v[1].toUpperCase() + ' (';
633
638
  for (const v1 of v[2]) {
634
639
  if (Array.isArray(v1)) {
@@ -647,7 +652,7 @@ export class Sql {
647
652
  sql = sql.slice(0, -2) + ') AND ';
648
653
  }
649
654
  else {
650
- // --- 2, 6 ---
655
+ // --- 普通运算符或字段比较 ---
651
656
  const nv = v[2];
652
657
  // --- v[0] 也可以是 value() 包裹的字面量值,而不一定是字段名 ---
653
658
  const isv0 = this._isValue(v[0]);
@@ -657,19 +662,23 @@ export class Sql {
657
662
  }
658
663
  const isf = this._isField(nv);
659
664
  if (isf) {
660
- // --- 6. field ---
665
+ // --- 字段比较 ---
661
666
  sql += v0sql + ' ' + v[1] + ' ' + this.field(nv.value) + ' AND ';
662
667
  }
663
668
  else {
664
- sql += v0sql + ' ' + v[1] + ' ' + this._placeholder() + ' AND ';
665
- data.push(nv);
669
+ const result = lSqlValue.isJson(nv) ? this._processValue(nv) : {
670
+ 'sql': this._placeholder(),
671
+ 'data': [nv],
672
+ };
673
+ sql += v0sql + ' ' + v[1] + ' ' + result.sql + ' AND ';
674
+ data.push(...result.data);
666
675
  }
667
676
  }
668
677
  }
669
678
  else {
670
- // --- 1, 4, 5, 6 ---
679
+ // --- 字段映射、逻辑分组与 IN 查询 ---
671
680
  if (k.startsWith('$')) {
672
- // --- 5 - '$or': [{'city': 'bj'}, {'city': 'sh'}] ---
681
+ // --- 逻辑条件分组,如 $or ---
673
682
  const sp = ' ' + k.slice(1).split('-')[0].toUpperCase() + ' ';
674
683
  sql += '(';
675
684
  for (let v1 of v) {
@@ -685,21 +694,28 @@ export class Sql {
685
694
  sql = sql.slice(0, -sp.length) + ') AND ';
686
695
  }
687
696
  else {
688
- // --- 1, 4, 6 ---
697
+ // --- 单字段条件 ---
689
698
  if (v === null) {
699
+ // --- NULL 判断 ---
690
700
  sql += this.field(k) + ' IS NULL AND ';
691
701
  }
692
702
  else if (typeof v === 'string' || typeof v === 'number') {
693
- // --- 1 ---
703
+ // --- 标量相等 ---
694
704
  sql += this.field(k) + ' = ' + this._placeholder() + ' AND ';
695
705
  data.push(v);
696
706
  }
707
+ else if (lSqlValue.isJson(v)) {
708
+ // --- JSON 相等 ---
709
+ const result = this._processValue(v);
710
+ sql += this.field(k) + ' = ' + result.sql + ' AND ';
711
+ data.push(...result.data);
712
+ }
697
713
  else if (this._isField(v)) {
698
- // --- 6 ---
714
+ // --- 字段相等 ---
699
715
  sql += this.field(k) + ' = ' + this.field(v.value) + ' AND ';
700
716
  }
701
717
  else {
702
- // --- 4 - 'type': ['1', '2'] ---
718
+ // --- IN 查询,如 'type': ['1', '2'] ---
703
719
  if (v.length > 0) {
704
720
  sql += this.field(k) + ' IN (';
705
721
  for (const v1 of v) {
@@ -935,7 +951,7 @@ export class Sql {
935
951
  * --- 获取全部 data ---
936
952
  */
937
953
  getData() {
938
- return this._data;
954
+ return lSqlValue.serializeList(this._data) ?? [];
939
955
  }
940
956
  /**
941
957
  * --- 获取定义的 pre ---
@@ -1096,48 +1112,6 @@ export class Sql {
1096
1112
  _placeholder() {
1097
1113
  return this._service === ESERVICE.MYSQL ? '?' : `$${this._placeholderCounter++}`;
1098
1114
  }
1099
- /**
1100
- * --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
1101
- * @param v 要处理的值
1102
- */
1103
- _pgCastSuffix(v) {
1104
- if (v === null || v === undefined) {
1105
- return '';
1106
- }
1107
- if (typeof v === 'number') {
1108
- return Number.isInteger(v) ? '::bigint' : '::float8';
1109
- }
1110
- if (typeof v === 'boolean') {
1111
- return '::boolean';
1112
- }
1113
- if (v instanceof Buffer) {
1114
- return '::bytea';
1115
- }
1116
- if (Array.isArray(v)) {
1117
- // --- 函数式语法 ['FUNC(?)', [...]],不加转换 ---
1118
- if (typeof v[0] === 'string' && v[0].includes('(')) {
1119
- return '';
1120
- }
1121
- // --- POLYGON ---
1122
- if (v[0]?.y !== undefined) {
1123
- return '::polygon';
1124
- }
1125
- // --- JSON 数组或 PG 原生数组(text[]、int[] 等),
1126
- // 不加转换,由 pg 驱动与目标列类型决定 ---
1127
- return '';
1128
- }
1129
- if (typeof v === 'object') {
1130
- // --- POINT ---
1131
- if (v.y !== undefined) {
1132
- return '::point';
1133
- }
1134
- // --- JSON 对象(用户应通过 sql.json() 包裹为字符串后传入),不加转换 ---
1135
- return '';
1136
- }
1137
- // --- string:保持 unknown 类型,兼容 text/varchar/jsonb 等目标列类型;
1138
- // 使用 sql.json() 包裹的 jsonb 数据经此路径,unknown 可隐式 cast 到 jsonb ---
1139
- return '';
1140
- }
1141
1115
  /**
1142
1116
  * --- 处理单个值,检测数据类型并返回 SQL 和 data ---
1143
1117
  * @param v 要处理的值
@@ -1151,6 +1125,12 @@ export class Sql {
1151
1125
  else if (v === null) {
1152
1126
  return { 'sql': 'NULL', 'data': [] };
1153
1127
  }
1128
+ else if (lSqlValue.isJson(v)) {
1129
+ return {
1130
+ 'sql': this._placeholder() + (this._service === ESERVICE.PGSQL ? '::jsonb' : ''),
1131
+ 'data': [lSqlValue.serialize(v)]
1132
+ };
1133
+ }
1154
1134
  else if (typeof v === 'string' || typeof v === 'number') {
1155
1135
  return { 'sql': this._placeholder(), 'data': [v] };
1156
1136
  }
@@ -1278,6 +1258,7 @@ export function get(opt) {
1278
1258
  * @param service 服务商,默认 MySQL
1279
1259
  */
1280
1260
  export function format(sql, data, service = ESERVICE.MYSQL) {
1261
+ data = lSqlValue.serializeList(data) ?? [];
1281
1262
  if (service === ESERVICE.MYSQL) {
1282
1263
  return mysql2.format(sql, data);
1283
1264
  }
@@ -1359,9 +1340,10 @@ export function value(val) {
1359
1340
  };
1360
1341
  }
1361
1342
  /**
1362
- * --- 将对象转换为 JSON 字符串并避开类型检查,用于适配 PostgreSQL 的 jsonb 字段 ---
1363
- * @param obj 要转换的 JSON 对象
1343
+ * --- 标记需要写入 JSON/jsonb 字段的值;实际序列化延迟到数据库边界 ---
1344
+ * @param obj 原始 JSON
1345
+ * @returns 类型保持不变的 JSON 包装值
1364
1346
  */
1365
1347
  export function json(obj) {
1366
- return lText.stringifyJson(obj);
1348
+ return lSqlValue.json(obj);
1367
1349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.15.0",
3
+ "version": "9.15.2",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [