befly 3.20.11 → 3.21.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/lib/dbHelper.js +1626 -0
- package/lib/dbUtil.js +988 -0
- package/lib/sqlBuilder.js +714 -0
- package/package.json +2 -2
- package/plugins/mysql.js +1 -1
- package/scripts/syncDb/context.js +1 -1
- package/sync/dev.js +3 -0
- package/lib/dbHelper/builders.js +0 -658
- package/lib/dbHelper/dataOps.js +0 -578
- package/lib/dbHelper/execute.js +0 -128
- package/lib/dbHelper/index.js +0 -24
- package/lib/dbHelper/util.js +0 -95
- package/lib/dbHelper/validate.js +0 -695
- package/lib/sqlBuilder/batch.js +0 -114
- package/lib/sqlBuilder/check.js +0 -82
- package/lib/sqlBuilder/compiler.js +0 -360
- package/lib/sqlBuilder/index.js +0 -200
- package/lib/sqlBuilder/parser.js +0 -305
- package/lib/sqlBuilder/util.js +0 -273
package/lib/dbUtil.js
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import { isFiniteNumber, isNonEmptyString, isNullable, isString } from "../utils/is.js";
|
|
2
|
+
import { snakeCase } from "../utils/util.js";
|
|
3
|
+
|
|
4
|
+
export const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
5
|
+
|
|
6
|
+
export function assertNoUndefinedParam(value, label) {
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
throw new Error(`${label} 不能为 undefined`, {
|
|
9
|
+
cause: null,
|
|
10
|
+
code: "validation"
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertSafeIdentifierPart(part, kind) {
|
|
16
|
+
if (!SAFE_IDENTIFIER_RE.test(part)) {
|
|
17
|
+
throw new Error(`无效的 ${kind} 标识符: ${part}`, {
|
|
18
|
+
cause: null,
|
|
19
|
+
code: "validation"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateQuoteIdentifierInput(identifier) {
|
|
25
|
+
if (!isString(identifier)) {
|
|
26
|
+
throw new Error(`quoteIdent 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
|
|
27
|
+
cause: null,
|
|
28
|
+
code: "validation"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = identifier.trim();
|
|
33
|
+
if (!trimmed) {
|
|
34
|
+
throw new Error("SQL 标识符不能为空", {
|
|
35
|
+
cause: null,
|
|
36
|
+
code: "validation"
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateLimitValue(count) {
|
|
44
|
+
if (typeof count !== "number" || count < 0) {
|
|
45
|
+
throw new Error(`LIMIT 数量必须是非负数 (count: ${String(count)})`, {
|
|
46
|
+
cause: null,
|
|
47
|
+
code: "validation"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateOffsetValue(offset, label = "OFFSET") {
|
|
53
|
+
if (typeof offset !== "number" || offset < 0) {
|
|
54
|
+
throw new Error(`${label} 必须是非负数 (offset: ${String(offset)})`, {
|
|
55
|
+
cause: null,
|
|
56
|
+
code: "validation"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateExcludeFieldsResult(resultFields, table, excludeSnakeFields) {
|
|
62
|
+
if (resultFields.length === 0) {
|
|
63
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`, {
|
|
64
|
+
cause: null,
|
|
65
|
+
code: "validation"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function assertNoExprField(field) {
|
|
71
|
+
if (!isString(field)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!isNonEmptyString(field)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const trimmed = field.trim();
|
|
79
|
+
if (trimmed.includes("(") || trimmed.includes(")")) {
|
|
80
|
+
throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
81
|
+
cause: null,
|
|
82
|
+
code: "validation"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function assertNoUndefinedInRecord(row, label) {
|
|
88
|
+
for (const [key, value] of Object.entries(row)) {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
throw new Error(`${label} 存在 undefined 字段值 (field: ${key})`, {
|
|
91
|
+
cause: null,
|
|
92
|
+
code: "validation"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function assertBatchInsertRowsConsistent(rows, options) {
|
|
99
|
+
if (!Array.isArray(rows)) {
|
|
100
|
+
throw new Error("批量插入 rows 必须是数组", {
|
|
101
|
+
cause: null,
|
|
102
|
+
code: "validation"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (rows.length === 0) {
|
|
106
|
+
throw new Error(`插入数据不能为空 (table: ${options.table})`, {
|
|
107
|
+
cause: null,
|
|
108
|
+
code: "validation"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const first = rows[0];
|
|
113
|
+
if (!first || typeof first !== "object" || Array.isArray(first)) {
|
|
114
|
+
throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`, {
|
|
115
|
+
cause: null,
|
|
116
|
+
code: "validation"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const fields = Object.keys(first);
|
|
121
|
+
if (fields.length === 0) {
|
|
122
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${options.table})`, {
|
|
123
|
+
cause: null,
|
|
124
|
+
code: "validation"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const fieldSet = new Set(fields);
|
|
129
|
+
for (let i = 0; i < rows.length; i++) {
|
|
130
|
+
const row = rows[i];
|
|
131
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
132
|
+
throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`, {
|
|
133
|
+
cause: null,
|
|
134
|
+
code: "validation"
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rowKeys = Object.keys(row);
|
|
139
|
+
if (rowKeys.length !== fields.length) {
|
|
140
|
+
throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`, {
|
|
141
|
+
cause: null,
|
|
142
|
+
code: "validation"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const key of rowKeys) {
|
|
147
|
+
if (!fieldSet.has(key)) {
|
|
148
|
+
throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`, {
|
|
149
|
+
cause: null,
|
|
150
|
+
code: "validation"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const field of fields) {
|
|
156
|
+
if (!(field in row)) {
|
|
157
|
+
throw new Error(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
|
|
158
|
+
cause: null,
|
|
159
|
+
code: "validation"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (row[field] === undefined) {
|
|
163
|
+
throw new Error(`批量插入字段值不能为 undefined (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
|
|
164
|
+
cause: null,
|
|
165
|
+
code: "validation"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return fields;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function parseTableRef(tableRef) {
|
|
175
|
+
if (!isString(tableRef)) {
|
|
176
|
+
throw new Error(`tableRef 必须是字符串 (tableRef: ${String(tableRef)})`, {
|
|
177
|
+
cause: null,
|
|
178
|
+
code: "validation"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const trimmed = tableRef.trim();
|
|
183
|
+
if (!isNonEmptyString(tableRef)) {
|
|
184
|
+
throw new Error("tableRef 不能为空", {
|
|
185
|
+
cause: null,
|
|
186
|
+
code: "validation"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parts = trimmed.split(/\s+/).filter((part) => part.length > 0);
|
|
191
|
+
if (parts.length === 0) {
|
|
192
|
+
throw new Error("tableRef 不能为空", {
|
|
193
|
+
cause: null,
|
|
194
|
+
code: "validation"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (parts.length > 2) {
|
|
198
|
+
throw new Error(`不支持的表引用格式(包含过多片段)。请使用最简形式:table 或 table alias 或 schema.table 或 schema.table alias (tableRef: ${trimmed})`, {
|
|
199
|
+
cause: null,
|
|
200
|
+
code: "validation"
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const namePart = parts[0];
|
|
205
|
+
if (!isNonEmptyString(namePart)) {
|
|
206
|
+
throw new Error(`tableRef 解析失败:缺少表名 (tableRef: ${trimmed})`, {
|
|
207
|
+
cause: null,
|
|
208
|
+
code: "validation"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let aliasPart = null;
|
|
213
|
+
if (parts.length === 2) {
|
|
214
|
+
const alias = parts[1];
|
|
215
|
+
if (!isNonEmptyString(alias)) {
|
|
216
|
+
throw new Error(`tableRef 解析失败:缺少 alias (tableRef: ${trimmed})`, {
|
|
217
|
+
cause: null,
|
|
218
|
+
code: "validation"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
aliasPart = alias;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nameSegments = namePart.split(".");
|
|
225
|
+
if (nameSegments.length > 2) {
|
|
226
|
+
throw new Error(`不支持的表引用格式(schema 层级过深) (tableRef: ${trimmed})`, {
|
|
227
|
+
cause: null,
|
|
228
|
+
code: "validation"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (nameSegments.length === 2) {
|
|
233
|
+
const schema = nameSegments[0];
|
|
234
|
+
const table = nameSegments[1];
|
|
235
|
+
if (!isNonEmptyString(schema)) {
|
|
236
|
+
throw new Error(`tableRef 解析失败:schema 为空 (tableRef: ${trimmed})`, {
|
|
237
|
+
cause: null,
|
|
238
|
+
code: "validation"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (!isNonEmptyString(table)) {
|
|
242
|
+
throw new Error(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`, {
|
|
243
|
+
cause: null,
|
|
244
|
+
code: "validation"
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return { schema: schema, table: table, alias: aliasPart };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const table = nameSegments[0];
|
|
251
|
+
if (!isNonEmptyString(table)) {
|
|
252
|
+
throw new Error(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`, {
|
|
253
|
+
cause: null,
|
|
254
|
+
code: "validation"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { schema: null, table: table, alias: aliasPart };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function validateAndClassifyFields(fields) {
|
|
262
|
+
if (!fields || fields.length === 0) {
|
|
263
|
+
return { type: "all", fields: [] };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (fields.some((field) => field === "*")) {
|
|
267
|
+
throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段", {
|
|
268
|
+
cause: null,
|
|
269
|
+
code: "validation"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (fields.some((field) => !isNonEmptyString(field))) {
|
|
274
|
+
throw new Error("fields 不能包含空字符串或无效值", {
|
|
275
|
+
cause: null,
|
|
276
|
+
code: "validation"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const rawField of fields) {
|
|
281
|
+
const checkField = rawField.startsWith("!") ? rawField.substring(1) : rawField;
|
|
282
|
+
assertNoExprField(checkField);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const includeFields = fields.filter((field) => !field.startsWith("!"));
|
|
286
|
+
const excludeFields = fields.filter((field) => field.startsWith("!"));
|
|
287
|
+
|
|
288
|
+
if (includeFields.length > 0 && excludeFields.length === 0) {
|
|
289
|
+
return { type: "include", fields: includeFields };
|
|
290
|
+
}
|
|
291
|
+
if (excludeFields.length > 0 && includeFields.length === 0) {
|
|
292
|
+
return { type: "exclude", fields: excludeFields.map((field) => field.substring(1)) };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw new Error('fields 不能同时包含普通字段和排除字段(! 开头)。只能使用以下3种方式之一:\n1. 空数组 [] 或不传(查询所有)\n2. 全部指定字段 ["id", "name"]\n3. 全部排除字段 ["!password", "!token"]', {
|
|
296
|
+
cause: null,
|
|
297
|
+
code: "validation"
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function validateExplicitReadFields(fields) {
|
|
302
|
+
const classified = validateAndClassifyFields(fields);
|
|
303
|
+
if (classified.type === "all") {
|
|
304
|
+
throw new Error("查询必须显式传 fields,不支持空 fields 或查询全部字段", {
|
|
305
|
+
cause: null,
|
|
306
|
+
code: "validation"
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return classified;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function validateExplicitLeftJoinFields(fields) {
|
|
314
|
+
const classified = validateAndClassifyFields(fields);
|
|
315
|
+
if (classified.type === "all") {
|
|
316
|
+
throw new Error("leftJoin 查询必须显式传 fields,不支持空 fields 或查询全部字段", {
|
|
317
|
+
cause: null,
|
|
318
|
+
code: "validation"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (classified.type === "exclude") {
|
|
322
|
+
throw new Error("leftJoin 查询不支持排除字段,必须显式指定主表/从表字段", {
|
|
323
|
+
cause: null,
|
|
324
|
+
code: "validation"
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return classified;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isQuotedIdentPaired(value) {
|
|
332
|
+
const trimmed = value.trim();
|
|
333
|
+
if (trimmed.length < 2) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const first = trimmed[0];
|
|
338
|
+
const last = trimmed[trimmed.length - 1];
|
|
339
|
+
return (first === "`" && last === "`") || (first === '"' && last === '"');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function startsWithIdentifierQuote(value) {
|
|
343
|
+
const trimmed = value.trim();
|
|
344
|
+
return trimmed.startsWith("`") || trimmed.startsWith('"');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function assertPairedQuotedIdent(value, label) {
|
|
348
|
+
if (startsWithIdentifierQuote(value) && !isQuotedIdentPaired(value)) {
|
|
349
|
+
throw new Error(`${label} 引用不完整,请使用成对的 \`...\` 或 "..." (value: ${value})`, {
|
|
350
|
+
cause: null,
|
|
351
|
+
code: "validation"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function escapeIdentifierPart(part, kind, quoteIdent, unpairedErrorFactory) {
|
|
357
|
+
if (isQuotedIdent(part)) {
|
|
358
|
+
return part;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (startsWithQuote(part)) {
|
|
362
|
+
throw new Error(unpairedErrorFactory(part), {
|
|
363
|
+
cause: null,
|
|
364
|
+
code: "validation"
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
assertSafeIdentifierPart(part, kind);
|
|
369
|
+
return quoteIdent(part);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function resolveQuoteIdent(options) {
|
|
373
|
+
if (options && options.quoteIdent) {
|
|
374
|
+
return options.quoteIdent;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return (identifier) => {
|
|
378
|
+
const trimmed = validateQuoteIdentifierInput(identifier);
|
|
379
|
+
|
|
380
|
+
const escaped = trimmed.replace(/`/g, "``");
|
|
381
|
+
return `\`${escaped}\``;
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function isQuotedIdent(value) {
|
|
386
|
+
return isQuotedIdentPaired(value);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function startsWithQuote(value) {
|
|
390
|
+
return startsWithIdentifierQuote(value);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function escapeField(field, quoteIdent) {
|
|
394
|
+
if (!isString(field)) {
|
|
395
|
+
return field;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const trimmed = field.trim();
|
|
399
|
+
|
|
400
|
+
assertPairedQuotedIdent(trimmed, "字段标识符");
|
|
401
|
+
|
|
402
|
+
if (trimmed === "*" || isQuotedIdent(trimmed)) {
|
|
403
|
+
return trimmed;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
assertNoExprField(trimmed);
|
|
408
|
+
} catch {
|
|
409
|
+
throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
410
|
+
cause: null,
|
|
411
|
+
code: "validation"
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (trimmed.toUpperCase().includes(" AS ")) {
|
|
416
|
+
const parts = trimmed.split(/\s+AS\s+/i);
|
|
417
|
+
if (parts.length !== 2) {
|
|
418
|
+
throw new Error(`字段格式非法,请使用简单字段名或安全引用,复杂表达式请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
419
|
+
cause: null,
|
|
420
|
+
code: "validation"
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const fieldPart = parts[0];
|
|
425
|
+
const aliasPart = parts[1];
|
|
426
|
+
const cleanFieldPart = fieldPart.trim();
|
|
427
|
+
const cleanAliasPart = aliasPart.trim();
|
|
428
|
+
if (!isQuotedIdent(cleanAliasPart)) {
|
|
429
|
+
if (!SAFE_IDENTIFIER_RE.test(cleanAliasPart)) {
|
|
430
|
+
throw new Error(`无效的字段别名: ${cleanAliasPart}`, {
|
|
431
|
+
cause: null,
|
|
432
|
+
code: "validation"
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return `${escapeField(cleanFieldPart, quoteIdent)} AS ${cleanAliasPart}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (trimmed.includes(".")) {
|
|
440
|
+
const parts = trimmed.split(".");
|
|
441
|
+
return parts
|
|
442
|
+
.map((part) => {
|
|
443
|
+
const cleanPart = part.trim();
|
|
444
|
+
if (cleanPart === "*" || isQuotedIdent(cleanPart)) {
|
|
445
|
+
return cleanPart;
|
|
446
|
+
}
|
|
447
|
+
assertPairedQuotedIdent(cleanPart, "字段标识符");
|
|
448
|
+
return quoteIdent(cleanPart);
|
|
449
|
+
})
|
|
450
|
+
.join(".");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return quoteIdent(trimmed);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function escapeTable(table, quoteIdent) {
|
|
457
|
+
if (!isString(table)) {
|
|
458
|
+
return table;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const trimmed = table.trim();
|
|
462
|
+
|
|
463
|
+
if (isQuotedIdent(trimmed)) {
|
|
464
|
+
return trimmed;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const parts = trimmed.split(/\s+/).filter((p) => p.length > 0);
|
|
468
|
+
if (parts.length === 0) {
|
|
469
|
+
throw new Error("FROM 表名不能为空", {
|
|
470
|
+
cause: null,
|
|
471
|
+
code: "validation"
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (parts.length > 2) {
|
|
476
|
+
throw new Error(`不支持的表引用格式(包含过多片段)。请使用 fromRaw 显式传入复杂表达式 (table: ${trimmed})`, {
|
|
477
|
+
cause: null,
|
|
478
|
+
code: "validation"
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const namePart = parts[0].trim();
|
|
483
|
+
|
|
484
|
+
const aliasPart = parts.length === 2 ? parts[1].trim() : null;
|
|
485
|
+
|
|
486
|
+
const nameSegments = namePart.split(".");
|
|
487
|
+
if (nameSegments.length > 2) {
|
|
488
|
+
throw new Error(`不支持的表引用格式(schema 层级过深)。请使用 fromRaw (table: ${trimmed})`, {
|
|
489
|
+
cause: null,
|
|
490
|
+
code: "validation"
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let escapedName = "";
|
|
495
|
+
if (nameSegments.length === 2) {
|
|
496
|
+
const schemaRaw = nameSegments[0];
|
|
497
|
+
const tableNameRaw = nameSegments[1];
|
|
498
|
+
const schema = schemaRaw.trim();
|
|
499
|
+
const tableName = tableNameRaw.trim();
|
|
500
|
+
|
|
501
|
+
const escapedSchema = escapeIdentifierPart(schema, "schema", quoteIdent, (part) => `schema 标识符引用不完整,请使用成对的 \`...\` 或 "..." (schema: ${part})`);
|
|
502
|
+
const escapedTableName = escapeIdentifierPart(tableName, "table", quoteIdent, (part) => `table 标识符引用不完整,请使用成对的 \`...\` 或 "..." (table: ${part})`);
|
|
503
|
+
|
|
504
|
+
escapedName = `${escapedSchema}.${escapedTableName}`;
|
|
505
|
+
} else {
|
|
506
|
+
const tableNameRaw = nameSegments[0];
|
|
507
|
+
const tableName = tableNameRaw.trim();
|
|
508
|
+
|
|
509
|
+
escapedName = escapeIdentifierPart(tableName, "table", quoteIdent, (part) => `table 标识符引用不完整,请使用成对的 \`...\` 或 "..." (table: ${part})`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (aliasPart) {
|
|
513
|
+
assertSafeIdentifierPart(aliasPart, "alias");
|
|
514
|
+
return `${escapedName} ${aliasPart}`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return escapedName;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function validateParam(value) {
|
|
521
|
+
assertNoUndefinedParam(value, "SQL 参数值");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function normalizeLimitValue(count, offset) {
|
|
525
|
+
validateLimitValue(count);
|
|
526
|
+
const limitValue = Math.floor(count);
|
|
527
|
+
|
|
528
|
+
let offsetValue = null;
|
|
529
|
+
if (!isNullable(offset)) {
|
|
530
|
+
validateOffsetValue(offset);
|
|
531
|
+
offsetValue = Math.floor(offset);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return { limitValue: limitValue, offsetValue: offsetValue };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function normalizeOffsetValue(count) {
|
|
538
|
+
validateOffsetValue(count, "OFFSET");
|
|
539
|
+
return Math.floor(count);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function toSqlParams(params) {
|
|
543
|
+
if (isNullable(params)) {
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!Array.isArray(params)) {
|
|
548
|
+
throw new Error(`SQL 参数必须是数组,当前类型: ${typeof params}`, {
|
|
549
|
+
cause: null,
|
|
550
|
+
code: "validation"
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const out = [];
|
|
555
|
+
for (const value of params) {
|
|
556
|
+
if (value === undefined) {
|
|
557
|
+
throw new Error("SQL 参数不能为 undefined", {
|
|
558
|
+
cause: null,
|
|
559
|
+
code: "validation"
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (typeof value === "bigint") {
|
|
564
|
+
out.push(value.toString());
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
out.push(value);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function normalizeTableRef(tableRef) {
|
|
575
|
+
const parsed = parseTableRef(tableRef);
|
|
576
|
+
const schemaPart = parsed.schema ? snakeCase(parsed.schema) : null;
|
|
577
|
+
const tablePart = snakeCase(parsed.table);
|
|
578
|
+
const aliasPart = parsed.alias ? snakeCase(parsed.alias) : null;
|
|
579
|
+
let result = schemaPart ? `${schemaPart}.${tablePart}` : tablePart;
|
|
580
|
+
if (aliasPart) {
|
|
581
|
+
result = `${result} ${aliasPart}`;
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function shouldExcludeFieldValue(field, value, excludeValueMap) {
|
|
587
|
+
if (!excludeValueMap || excludeValueMap.size === 0) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const candidateValues = excludeValueMap.get(field);
|
|
592
|
+
if (!candidateValues || candidateValues.length === 0) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
for (const candidate of candidateValues) {
|
|
597
|
+
if (value === candidate) {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function mergeExcludeValueMap(parentMap, excludeConfig) {
|
|
606
|
+
const result = new Map();
|
|
607
|
+
|
|
608
|
+
if (parentMap instanceof Map) {
|
|
609
|
+
for (const [key, values] of parentMap.entries()) {
|
|
610
|
+
result.set(key, Array.isArray(values) ? [...values] : []);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (!excludeConfig || typeof excludeConfig !== "object" || Array.isArray(excludeConfig)) {
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const [key, values] of Object.entries(excludeConfig)) {
|
|
619
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const current = result.get(key) || [];
|
|
624
|
+
result.set(key, [...current, ...values]);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function clearDeep(value, options) {
|
|
631
|
+
const arrayObjectKeys = Array.isArray(options?.arrayObjectKeys) ? options.arrayObjectKeys : ["$or", "$and"];
|
|
632
|
+
const arrayObjectKeySet = new Set();
|
|
633
|
+
for (const key of arrayObjectKeys) {
|
|
634
|
+
arrayObjectKeySet.add(key);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const depthRaw = isFiniteNumber(options?.depth) ? Math.floor(options.depth) : 0;
|
|
638
|
+
const depth = depthRaw < 0 ? 0 : depthRaw;
|
|
639
|
+
|
|
640
|
+
const clearInternal = (input, remainingDepth, inheritedExcludeValueMap) => {
|
|
641
|
+
if (!input || typeof input !== "object") {
|
|
642
|
+
return input;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (Array.isArray(input)) {
|
|
646
|
+
return input;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const canRecurse = remainingDepth === 0 || remainingDepth > 1;
|
|
650
|
+
const childDepth = remainingDepth === 0 ? 0 : remainingDepth - 1;
|
|
651
|
+
const result = {};
|
|
652
|
+
const currentExcludeValueMap = mergeExcludeValueMap(inheritedExcludeValueMap, input.$exclude);
|
|
653
|
+
|
|
654
|
+
for (const [key, item] of Object.entries(input)) {
|
|
655
|
+
if (key === "$exclude") {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (isNullable(item)) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (shouldExcludeFieldValue(key, item, currentExcludeValueMap)) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (arrayObjectKeySet.has(key)) {
|
|
668
|
+
if (!Array.isArray(item)) {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const outList = [];
|
|
673
|
+
for (const child of item) {
|
|
674
|
+
if (!child || typeof child !== "object" || Array.isArray(child)) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const cleaned = clearInternal(child, remainingDepth, currentExcludeValueMap);
|
|
678
|
+
if (!cleaned || typeof cleaned !== "object" || Array.isArray(cleaned)) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (Object.keys(cleaned).length === 0) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
outList.push(cleaned);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (outList.length > 0) {
|
|
688
|
+
result[key] = outList;
|
|
689
|
+
}
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (typeof item === "object" && !Array.isArray(item)) {
|
|
694
|
+
if (!canRecurse) {
|
|
695
|
+
result[key] = item;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const nested = clearInternal(item, childDepth, currentExcludeValueMap);
|
|
700
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (Object.keys(nested).length === 0) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
result[key] = nested;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
result[key] = item;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return result;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
return clearInternal(value, depth, new Map());
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export async function fieldsToSnake(table, classified, getTableColumns) {
|
|
720
|
+
if (classified.type === "include") {
|
|
721
|
+
return classified.fields.map((field) => processJoinField(field));
|
|
722
|
+
}
|
|
723
|
+
if (classified.type === "exclude") {
|
|
724
|
+
const allColumns = await getTableColumns(table);
|
|
725
|
+
const excludeSnakeFields = classified.fields.map((field) => snakeCase(field));
|
|
726
|
+
const resultFields = allColumns.filter((column) => !excludeSnakeFields.includes(column));
|
|
727
|
+
validateExcludeFieldsResult(resultFields, table, excludeSnakeFields);
|
|
728
|
+
return resultFields;
|
|
729
|
+
}
|
|
730
|
+
return ["*"];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function normalizeQualifierField(field) {
|
|
734
|
+
const parts = field.split(".");
|
|
735
|
+
if (parts.length === 1) {
|
|
736
|
+
return snakeCase(field);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const fieldName = parts[parts.length - 1].trim();
|
|
740
|
+
const qualifier = parts
|
|
741
|
+
.slice(0, parts.length - 1)
|
|
742
|
+
.map((item) => snakeCase(item.trim()))
|
|
743
|
+
.join(".");
|
|
744
|
+
|
|
745
|
+
if (fieldName === "*") {
|
|
746
|
+
return `${qualifier}.*`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return `${qualifier}.${snakeCase(fieldName)}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function normalizeOrderBy(orderBy, mapField) {
|
|
753
|
+
if (!orderBy || !Array.isArray(orderBy)) {
|
|
754
|
+
return orderBy;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return orderBy.map((item) => {
|
|
758
|
+
if (!isString(item) || !item.includes("#")) {
|
|
759
|
+
return item;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const parts = item.split("#");
|
|
763
|
+
if (parts.length !== 2) {
|
|
764
|
+
return item;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return `${mapField(parts[0].trim())}#${parts[1].trim()}`;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function mapWhereKeys(where, mapKey) {
|
|
772
|
+
if (!where || typeof where !== "object") {
|
|
773
|
+
return where;
|
|
774
|
+
}
|
|
775
|
+
if (Array.isArray(where)) {
|
|
776
|
+
return where.map((item) => mapWhereKeys(item, mapKey));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const result = {};
|
|
780
|
+
for (const [key, value] of Object.entries(where)) {
|
|
781
|
+
if (key === "$or" || key === "$and") {
|
|
782
|
+
result[key] = Array.isArray(value) ? value.map((item) => mapWhereKeys(item, mapKey)) : [];
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const normalizedKey = mapKey(key);
|
|
787
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
788
|
+
result[normalizedKey] = mapWhereKeys(value, mapKey);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
result[normalizedKey] = value;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export function orderByToSnake(orderBy) {
|
|
799
|
+
return normalizeOrderBy(orderBy, (field) => snakeCase(field));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function processJoinField(field) {
|
|
803
|
+
assertNoExprField(field);
|
|
804
|
+
if (field === "*" || field.startsWith("`")) {
|
|
805
|
+
return field;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (field.toUpperCase().includes(" AS ")) {
|
|
809
|
+
const parts = field.split(/\s+AS\s+/i);
|
|
810
|
+
const fieldPart = parts[0];
|
|
811
|
+
const aliasPart = parts[1];
|
|
812
|
+
if (!isString(fieldPart) || !isString(aliasPart)) {
|
|
813
|
+
return field;
|
|
814
|
+
}
|
|
815
|
+
return `${processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return normalizeQualifierField(field);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function processJoinWhereKey(key) {
|
|
822
|
+
if (key === "$or" || key === "$and") {
|
|
823
|
+
return key;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
assertNoExprField(key);
|
|
827
|
+
if (key.includes("$")) {
|
|
828
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
829
|
+
const fieldPart = key.substring(0, lastDollarIndex);
|
|
830
|
+
const operator = key.substring(lastDollarIndex);
|
|
831
|
+
|
|
832
|
+
return `${normalizeQualifierField(fieldPart)}${operator}`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return normalizeQualifierField(key);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export function processJoinWhere(where) {
|
|
839
|
+
return mapWhereKeys(where, processJoinWhereKey);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function processJoinOrderBy(orderBy) {
|
|
843
|
+
return normalizeOrderBy(orderBy, processJoinField);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function processJoinOn(on) {
|
|
847
|
+
if (!isString(on)) {
|
|
848
|
+
return String(on);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const raw = String(on);
|
|
852
|
+
if (!isNonEmptyString(raw)) {
|
|
853
|
+
return raw;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const replaceSegment = (segment) => {
|
|
857
|
+
return segment.replace(/([a-zA-Z_][a-zA-Z0-9_]*)(\s*\.\s*)([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, left, dot, right) => {
|
|
858
|
+
return `${snakeCase(left)}${dot}${snakeCase(right)}`;
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
let result = "";
|
|
863
|
+
let buffer = "";
|
|
864
|
+
let quote = null;
|
|
865
|
+
let prev = "";
|
|
866
|
+
|
|
867
|
+
for (const ch of raw) {
|
|
868
|
+
if (quote) {
|
|
869
|
+
result += ch;
|
|
870
|
+
if (ch === quote && prev !== "\\") {
|
|
871
|
+
quote = null;
|
|
872
|
+
}
|
|
873
|
+
prev = ch;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
878
|
+
if (buffer.length > 0) {
|
|
879
|
+
result += replaceSegment(buffer);
|
|
880
|
+
buffer = "";
|
|
881
|
+
}
|
|
882
|
+
quote = ch;
|
|
883
|
+
result += ch;
|
|
884
|
+
prev = ch;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
buffer += ch;
|
|
889
|
+
prev = ch;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (buffer.length > 0) {
|
|
893
|
+
result += replaceSegment(buffer);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function parseLeftJoinItem(joinTable, joinItem) {
|
|
900
|
+
const parts = joinItem.split(" ");
|
|
901
|
+
return {
|
|
902
|
+
type: "left",
|
|
903
|
+
table: normalizeTableRef(joinTable),
|
|
904
|
+
on: processJoinOn(`${parts[0]} = ${parts[1]}`)
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function addDefaultStateFilter(where = {}, table, hasLeftJoin = false, beflyMode = "auto") {
|
|
909
|
+
if (beflyMode === "manual") {
|
|
910
|
+
return where;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
|
|
914
|
+
if (hasStateCondition) {
|
|
915
|
+
return where;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const result = {};
|
|
919
|
+
for (const [key, value] of Object.entries(where)) {
|
|
920
|
+
result[key] = value;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (hasLeftJoin && table) {
|
|
924
|
+
result[`${table}.state$gt`] = 0;
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
result.state$gt = 0;
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function whereKeysToSnake(where) {
|
|
933
|
+
return mapWhereKeys(where, (key) => {
|
|
934
|
+
assertNoExprField(key);
|
|
935
|
+
if (key.includes("$")) {
|
|
936
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
937
|
+
const fieldName = key.substring(0, lastDollarIndex);
|
|
938
|
+
const operator = key.substring(lastDollarIndex);
|
|
939
|
+
return `${snakeCase(fieldName)}${operator}`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return snakeCase(key);
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export function serializeArrayFields(data) {
|
|
947
|
+
const serialized = {};
|
|
948
|
+
for (const [key, value] of Object.entries(data)) {
|
|
949
|
+
if (isNullable(value)) {
|
|
950
|
+
serialized[key] = value;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (Array.isArray(value)) {
|
|
954
|
+
serialized[key] = JSON.stringify(value);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
serialized[key] = value;
|
|
958
|
+
}
|
|
959
|
+
return serialized;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function deserializeArrayFields(data) {
|
|
963
|
+
if (!data) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
const deserialized = {};
|
|
967
|
+
for (const [key, value] of Object.entries(data)) {
|
|
968
|
+
deserialized[key] = value;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
for (const [key, value] of Object.entries(deserialized)) {
|
|
972
|
+
if (!isString(value)) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
976
|
+
try {
|
|
977
|
+
const parsed = JSON.parse(value);
|
|
978
|
+
if (Array.isArray(parsed)) {
|
|
979
|
+
deserialized[key] = parsed;
|
|
980
|
+
}
|
|
981
|
+
} catch {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return deserialized;
|
|
988
|
+
}
|