@thornfe/jql2iql 0.0.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/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/index.cjs +1532 -0
- package/dist/index.d.cts +221 -0
- package/dist/index.d.mts +221 -0
- package/dist/index.mjs +1526 -0
- package/package.json +73 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1526 @@
|
|
|
1
|
+
import { CharStreams, CommonTokenStream } from "antlr4ts";
|
|
2
|
+
import { JQLLexer, JQLParser } from "@atlaskit/jql-parser";
|
|
3
|
+
import { TerminalNode } from "antlr4ts/tree/TerminalNode";
|
|
4
|
+
import { RuleNode } from "antlr4ts/tree/RuleNode";
|
|
5
|
+
|
|
6
|
+
//#region src/constants.ts
|
|
7
|
+
/**
|
|
8
|
+
* JQL 解析树节点类型常量
|
|
9
|
+
*/
|
|
10
|
+
const JQL_CONTEXT = {
|
|
11
|
+
TERMINAL_CLAUSE: "JqlTerminalClauseContext",
|
|
12
|
+
NOT_CLAUSE: "JqlNotClauseContext",
|
|
13
|
+
OR_CLAUSE: "JqlOrClauseContext",
|
|
14
|
+
AND_CLAUSE: "JqlAndClauseContext",
|
|
15
|
+
LIST: "JqlListContext",
|
|
16
|
+
FUNCTION: "JqlFunctionContext",
|
|
17
|
+
EMPTY: "JqlEmptyContext",
|
|
18
|
+
VALUE: "JqlValueContext",
|
|
19
|
+
OPERAND: "JqlOperandContext",
|
|
20
|
+
QUERY: "JqlQueryContext",
|
|
21
|
+
WHERE: "JqlWhereContext",
|
|
22
|
+
ORDER_BY: "JqlOrderByContext",
|
|
23
|
+
SEARCH_SORT: "JqlSearchSortContext",
|
|
24
|
+
FUNCTION_NAME: "JqlFunctionNameContext",
|
|
25
|
+
ARGUMENT_LIST: "JqlArgumentListContext",
|
|
26
|
+
ARGUMENT: "JqlArgumentContext",
|
|
27
|
+
EQUALS_CLAUSE: "JqlEqualsClauseContext",
|
|
28
|
+
COMPARISON_CLAUSE: "JqlComparisonClauseContext",
|
|
29
|
+
IN_CLAUSE: "JqlInClauseContext",
|
|
30
|
+
LIKE_CLAUSE: "JqlLikeClauseContext",
|
|
31
|
+
IS_CLAUSE: "JqlIsClauseContext",
|
|
32
|
+
STRING: "JqlStringContext",
|
|
33
|
+
NUMBER: "JqlNumberContext"
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 包装节点类型(透明传递给子节点)
|
|
37
|
+
*/
|
|
38
|
+
const WRAPPER_CONTEXTS = [
|
|
39
|
+
"JqlStringContext",
|
|
40
|
+
"JqlNumberContext",
|
|
41
|
+
"JqlNonNumberFieldContext",
|
|
42
|
+
"JqlListStartContext",
|
|
43
|
+
"JqlListEndContext",
|
|
44
|
+
"JqlEqualsOperatorContext",
|
|
45
|
+
"JqlComparisonOperatorContext",
|
|
46
|
+
"JqlInOperatorContext",
|
|
47
|
+
"JqlLikeOperatorContext",
|
|
48
|
+
"JqlFunctionNameContext",
|
|
49
|
+
"JqlArgumentContext"
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* 逻辑操作符常量
|
|
53
|
+
*/
|
|
54
|
+
const LOGICAL_OPERATORS = {
|
|
55
|
+
AND: "AND",
|
|
56
|
+
OR: "OR",
|
|
57
|
+
NOT: "NOT"
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* IQL 逻辑操作符(小写)
|
|
61
|
+
*/
|
|
62
|
+
const IQL_LOGICAL_OPERATORS = {
|
|
63
|
+
AND: "and",
|
|
64
|
+
OR: "or",
|
|
65
|
+
NOT: "not"
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 比较操作符常量
|
|
69
|
+
*/
|
|
70
|
+
const COMPARISON_OPERATORS = {
|
|
71
|
+
EQUAL: "=",
|
|
72
|
+
NOT_EQUAL: "!=",
|
|
73
|
+
GREATER_THAN: ">",
|
|
74
|
+
GREATER_THAN_OR_EQUAL: ">=",
|
|
75
|
+
LESS_THAN: "<",
|
|
76
|
+
LESS_THAN_OR_EQUAL: "<=",
|
|
77
|
+
LIKE: "~",
|
|
78
|
+
NOT_LIKE: "!~"
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 集合操作符常量
|
|
82
|
+
*/
|
|
83
|
+
const SET_OPERATORS = {
|
|
84
|
+
IN: "in",
|
|
85
|
+
NOT_IN: "not in"
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* 空值操作符常量
|
|
89
|
+
*/
|
|
90
|
+
const NULL_OPERATORS = {
|
|
91
|
+
IS: "is",
|
|
92
|
+
IS_NOT: "is not"
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* 空值关键字
|
|
96
|
+
*/
|
|
97
|
+
const NULL_KEYWORDS = {
|
|
98
|
+
NULL: "null",
|
|
99
|
+
NULL_UPPER: "NULL",
|
|
100
|
+
EMPTY: "EMPTY",
|
|
101
|
+
EMPTY_LOWER: "empty"
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 特殊函数名称
|
|
105
|
+
*/
|
|
106
|
+
const FUNCTION_NAMES = {
|
|
107
|
+
CURRENT_USER: "currentUser",
|
|
108
|
+
MEMBERS_OF: "membersOf",
|
|
109
|
+
CASCADE_OPTION: "cascadeOption"
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* SQL 子句关键字
|
|
113
|
+
*/
|
|
114
|
+
const SQL_CLAUSES = {
|
|
115
|
+
ORDER_BY: "order by",
|
|
116
|
+
ASC: "asc",
|
|
117
|
+
DESC: "desc"
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* AST 节点类型
|
|
121
|
+
*/
|
|
122
|
+
const AST_NODE_TYPES = {
|
|
123
|
+
BINARY_EXPRESSION: "BinaryExpression",
|
|
124
|
+
LOGICAL_EXPRESSION: "LogicalExpression",
|
|
125
|
+
JQL_QUERY: "JQLQuery",
|
|
126
|
+
ORDER_BY_CLAUSE: "OrderByClause",
|
|
127
|
+
LITERAL: "Literal",
|
|
128
|
+
FUNCTION: "Function",
|
|
129
|
+
LIST: "List"
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* 支持的字段类型列表
|
|
133
|
+
*/
|
|
134
|
+
const SUPPORTED_FIELD_TYPES = {
|
|
135
|
+
createdBy: "createdBy",
|
|
136
|
+
updatedBy: "updatedBy",
|
|
137
|
+
Assignee: "Assignee",
|
|
138
|
+
Reporter: "Reporter",
|
|
139
|
+
User: "User",
|
|
140
|
+
Status: "Status",
|
|
141
|
+
Version: "Version",
|
|
142
|
+
CustomVersion: "CustomVersion",
|
|
143
|
+
Sprint: "Sprint",
|
|
144
|
+
Priority: "Priority",
|
|
145
|
+
Workspace: "Workspace",
|
|
146
|
+
ItemType: "ItemType",
|
|
147
|
+
Name: "Name",
|
|
148
|
+
Text: "Text",
|
|
149
|
+
LongText: "LongText",
|
|
150
|
+
Key: "Key",
|
|
151
|
+
Number: "Number",
|
|
152
|
+
Integer: "Integer",
|
|
153
|
+
Float: "Float",
|
|
154
|
+
StoryPoint: "StoryPoint",
|
|
155
|
+
Date: "Date",
|
|
156
|
+
createdAt: "createdAt",
|
|
157
|
+
updatedAt: "updatedAt",
|
|
158
|
+
UserGroup: "UserGroup",
|
|
159
|
+
Tree: "Tree",
|
|
160
|
+
Tag: "Tag",
|
|
161
|
+
Cascade: "Cascade",
|
|
162
|
+
Checkbox: "Checkbox",
|
|
163
|
+
Dropdown: "Dropdown",
|
|
164
|
+
Radio: "Radio"
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/jql-to-ast/parser-utils.ts
|
|
169
|
+
const OPERATOR_SET = new Set([
|
|
170
|
+
COMPARISON_OPERATORS.EQUAL,
|
|
171
|
+
COMPARISON_OPERATORS.NOT_EQUAL,
|
|
172
|
+
COMPARISON_OPERATORS.GREATER_THAN,
|
|
173
|
+
COMPARISON_OPERATORS.LESS_THAN,
|
|
174
|
+
COMPARISON_OPERATORS.GREATER_THAN_OR_EQUAL,
|
|
175
|
+
COMPARISON_OPERATORS.LESS_THAN_OR_EQUAL,
|
|
176
|
+
COMPARISON_OPERATORS.LIKE,
|
|
177
|
+
COMPARISON_OPERATORS.NOT_LIKE,
|
|
178
|
+
SET_OPERATORS.IN,
|
|
179
|
+
SET_OPERATORS.NOT_IN
|
|
180
|
+
]);
|
|
181
|
+
new Set([
|
|
182
|
+
LOGICAL_OPERATORS.AND.toLowerCase(),
|
|
183
|
+
LOGICAL_OPERATORS.OR.toLowerCase(),
|
|
184
|
+
LOGICAL_OPERATORS.NOT.toLowerCase()
|
|
185
|
+
]);
|
|
186
|
+
const CLAUSE_OPERATORS = {
|
|
187
|
+
[JQL_CONTEXT.EQUALS_CLAUSE]: [COMPARISON_OPERATORS.EQUAL, COMPARISON_OPERATORS.NOT_EQUAL],
|
|
188
|
+
[JQL_CONTEXT.COMPARISON_CLAUSE]: [
|
|
189
|
+
COMPARISON_OPERATORS.GREATER_THAN,
|
|
190
|
+
COMPARISON_OPERATORS.LESS_THAN,
|
|
191
|
+
COMPARISON_OPERATORS.GREATER_THAN_OR_EQUAL,
|
|
192
|
+
COMPARISON_OPERATORS.LESS_THAN_OR_EQUAL
|
|
193
|
+
],
|
|
194
|
+
[JQL_CONTEXT.IN_CLAUSE]: [SET_OPERATORS.IN, SET_OPERATORS.NOT_IN],
|
|
195
|
+
[JQL_CONTEXT.LIKE_CLAUSE]: [COMPARISON_OPERATORS.LIKE, COMPARISON_OPERATORS.NOT_LIKE],
|
|
196
|
+
[JQL_CONTEXT.IS_CLAUSE]: [NULL_OPERATORS.IS, NULL_OPERATORS.IS_NOT]
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* 遍历 JQL 解析树
|
|
200
|
+
* @param tree 解析树根节点
|
|
201
|
+
* @param callback 遍历回调函数,返回 false 可以跳过子节点
|
|
202
|
+
* @param depth 当前深度(内部使用)
|
|
203
|
+
* @param parent 父节点(内部使用)
|
|
204
|
+
*/
|
|
205
|
+
function traverseJQLTree(tree, callback, depth = 0, parent) {
|
|
206
|
+
if (callback(tree, depth, parent) === false) return;
|
|
207
|
+
const childCount = tree.childCount;
|
|
208
|
+
for (let i = 0; i < childCount; i++) traverseJQLTree(tree.getChild(i), callback, depth + 1, tree);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 获取节点信息
|
|
212
|
+
*/
|
|
213
|
+
function getNodeInfo(node) {
|
|
214
|
+
if (node instanceof TerminalNode) {
|
|
215
|
+
var _node$symbol$type;
|
|
216
|
+
return {
|
|
217
|
+
type: "terminal",
|
|
218
|
+
text: node.text,
|
|
219
|
+
tokenType: (_node$symbol$type = node.symbol.type) === null || _node$symbol$type === void 0 ? void 0 : _node$symbol$type.toString()
|
|
220
|
+
};
|
|
221
|
+
} else if (node instanceof RuleNode) return {
|
|
222
|
+
type: "rule",
|
|
223
|
+
text: node.text,
|
|
224
|
+
ruleName: node.constructor.name
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
type: "rule",
|
|
228
|
+
text: node.text
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 获取所有子节点
|
|
233
|
+
*/
|
|
234
|
+
function getChildren(node) {
|
|
235
|
+
const children = [];
|
|
236
|
+
for (let i = 0; i < node.childCount; i++) children.push(node.getChild(i));
|
|
237
|
+
return children;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 提取字段名或简单值
|
|
241
|
+
*/
|
|
242
|
+
function extractFieldValue(node) {
|
|
243
|
+
if (!node) return null;
|
|
244
|
+
if (node instanceof TerminalNode) return node.text;
|
|
245
|
+
if (node instanceof RuleNode) {
|
|
246
|
+
const ruleName = node.constructor.name;
|
|
247
|
+
if (ruleName === JQL_CONTEXT.STRING || ruleName === JQL_CONTEXT.NUMBER) return node.text;
|
|
248
|
+
}
|
|
249
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
250
|
+
const value = extractFieldValue(node.getChild(i));
|
|
251
|
+
if (value) return value;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 提取操作符
|
|
257
|
+
*/
|
|
258
|
+
function extractOperator(node) {
|
|
259
|
+
var _constructor;
|
|
260
|
+
if (!node) return null;
|
|
261
|
+
if (node instanceof TerminalNode) {
|
|
262
|
+
const text = node.text;
|
|
263
|
+
const lowerText = text.toLowerCase();
|
|
264
|
+
return OPERATOR_SET.has(text) || OPERATOR_SET.has(lowerText) ? text : null;
|
|
265
|
+
}
|
|
266
|
+
const operators = CLAUSE_OPERATORS[((_constructor = node.constructor) === null || _constructor === void 0 ? void 0 : _constructor.name) || ""];
|
|
267
|
+
if (operators) return extractOperatorFromClause(node, operators);
|
|
268
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
269
|
+
const result = extractOperator(node.getChild(i));
|
|
270
|
+
if (result) return result;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 从特定类型的子句中提取操作符
|
|
276
|
+
*/
|
|
277
|
+
function extractOperatorFromClause(node, possibleOperators) {
|
|
278
|
+
let multiWordOps = null;
|
|
279
|
+
const singleWordOps = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const op of possibleOperators) if (op.includes(" ")) {
|
|
281
|
+
if (!multiWordOps) multiWordOps = [];
|
|
282
|
+
multiWordOps.push(op);
|
|
283
|
+
} else singleWordOps.add(op.toLowerCase());
|
|
284
|
+
const tokens = [];
|
|
285
|
+
const collectTokens = (n) => {
|
|
286
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
287
|
+
const child = n.getChild(i);
|
|
288
|
+
if (child instanceof TerminalNode) tokens.push(child.text.toLowerCase());
|
|
289
|
+
else collectTokens(child);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
if (multiWordOps) {
|
|
293
|
+
collectTokens(node);
|
|
294
|
+
if (tokens.length > 1) for (const op of multiWordOps) {
|
|
295
|
+
const opTokens = op.toLowerCase().split(" ");
|
|
296
|
+
const opLen = opTokens.length;
|
|
297
|
+
for (let i = 0; i <= tokens.length - opLen; i++) {
|
|
298
|
+
let match = true;
|
|
299
|
+
for (let j = 0; j < opLen; j++) if (tokens[i + j] !== opTokens[j]) {
|
|
300
|
+
match = false;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
if (match) return op;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
308
|
+
const child = node.getChild(i);
|
|
309
|
+
if (child instanceof TerminalNode) {
|
|
310
|
+
const text = child.text;
|
|
311
|
+
const lowerText = text.toLowerCase();
|
|
312
|
+
if (singleWordOps.has(lowerText) || singleWordOps.has(text)) return text;
|
|
313
|
+
} else {
|
|
314
|
+
const result = extractOperatorFromClause(child, possibleOperators);
|
|
315
|
+
if (result) return result;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/jql-to-ast/ast-builder.ts
|
|
323
|
+
const KEYWORDS = new Set([
|
|
324
|
+
LOGICAL_OPERATORS.AND,
|
|
325
|
+
LOGICAL_OPERATORS.OR,
|
|
326
|
+
LOGICAL_OPERATORS.NOT
|
|
327
|
+
]);
|
|
328
|
+
const OPERATORS = new Set([
|
|
329
|
+
COMPARISON_OPERATORS.EQUAL,
|
|
330
|
+
COMPARISON_OPERATORS.NOT_EQUAL,
|
|
331
|
+
COMPARISON_OPERATORS.GREATER_THAN,
|
|
332
|
+
COMPARISON_OPERATORS.LESS_THAN,
|
|
333
|
+
COMPARISON_OPERATORS.GREATER_THAN_OR_EQUAL,
|
|
334
|
+
COMPARISON_OPERATORS.LESS_THAN_OR_EQUAL,
|
|
335
|
+
COMPARISON_OPERATORS.LIKE,
|
|
336
|
+
COMPARISON_OPERATORS.NOT_LIKE
|
|
337
|
+
]);
|
|
338
|
+
const SKIP_TERMINALS = new Set([
|
|
339
|
+
"(",
|
|
340
|
+
")",
|
|
341
|
+
","
|
|
342
|
+
]);
|
|
343
|
+
/**
|
|
344
|
+
* 将 JQL 解析树转换为精简的 AST
|
|
345
|
+
*/
|
|
346
|
+
function buildAST(node) {
|
|
347
|
+
if (!node) return null;
|
|
348
|
+
const info = getNodeInfo(node);
|
|
349
|
+
if (node instanceof TerminalNode) {
|
|
350
|
+
const text = node.text;
|
|
351
|
+
if (KEYWORDS.has(text.toUpperCase()) || OPERATORS.has(text)) return null;
|
|
352
|
+
return {
|
|
353
|
+
type: "Literal",
|
|
354
|
+
value: text
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const ruleName = info.ruleName || "";
|
|
358
|
+
if (ruleName === JQL_CONTEXT.TERMINAL_CLAUSE) return buildTerminalClauseAST(node);
|
|
359
|
+
if (ruleName === JQL_CONTEXT.NOT_CLAUSE) return buildNotClauseAST(node);
|
|
360
|
+
if (ruleName === JQL_CONTEXT.OR_CLAUSE) return buildOrClauseAST(node);
|
|
361
|
+
if (ruleName === JQL_CONTEXT.AND_CLAUSE) return buildAndClauseAST(node);
|
|
362
|
+
if (ruleName === JQL_CONTEXT.LIST) return buildListAST(node);
|
|
363
|
+
if (ruleName === JQL_CONTEXT.FUNCTION) return buildFunctionAST(node);
|
|
364
|
+
if (ruleName === JQL_CONTEXT.EMPTY) return {
|
|
365
|
+
type: "Literal",
|
|
366
|
+
value: "EMPTY"
|
|
367
|
+
};
|
|
368
|
+
if (ruleName === JQL_CONTEXT.VALUE) {
|
|
369
|
+
const value = extractFieldValue(node);
|
|
370
|
+
return value ? {
|
|
371
|
+
type: "Literal",
|
|
372
|
+
value
|
|
373
|
+
} : null;
|
|
374
|
+
}
|
|
375
|
+
if (ruleName === JQL_CONTEXT.OPERAND) {
|
|
376
|
+
const children$1 = getChildren(node);
|
|
377
|
+
for (let i = 0; i < children$1.length; i++) {
|
|
378
|
+
const result = buildAST(children$1[i]);
|
|
379
|
+
if (result) return result;
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
if (ruleName === JQL_CONTEXT.QUERY) return buildQueryAST(node);
|
|
384
|
+
if (ruleName === JQL_CONTEXT.WHERE || WRAPPER_CONTEXTS.includes(ruleName)) {
|
|
385
|
+
const children$1 = getChildren(node);
|
|
386
|
+
for (let i = 0; i < children$1.length; i++) {
|
|
387
|
+
const result = buildAST(children$1[i]);
|
|
388
|
+
if (result) return result;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const children = getChildren(node);
|
|
393
|
+
for (let i = 0; i < children.length; i++) {
|
|
394
|
+
const result = buildAST(children[i]);
|
|
395
|
+
if (result) return result;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 构建终端子句 AST (field operator value)
|
|
401
|
+
*/
|
|
402
|
+
function buildTerminalClauseAST(node) {
|
|
403
|
+
const children = getChildren(node);
|
|
404
|
+
if (children.length >= 2) {
|
|
405
|
+
const field = extractFieldValue(children[0]);
|
|
406
|
+
const operator = extractOperator(children[1]);
|
|
407
|
+
const value = extractComplexValue(children[1], operator);
|
|
408
|
+
if (field && operator && value) return {
|
|
409
|
+
type: "BinaryExpression",
|
|
410
|
+
operator,
|
|
411
|
+
left: {
|
|
412
|
+
type: "Field",
|
|
413
|
+
value: field
|
|
414
|
+
},
|
|
415
|
+
right: value
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* 构建 NOT 子句 AST
|
|
422
|
+
*/
|
|
423
|
+
function buildNotClauseAST(node) {
|
|
424
|
+
const children = getChildren(node);
|
|
425
|
+
for (let i = 0; i < children.length; i++) if (children[i] instanceof TerminalNode && children[i].text.toUpperCase() === LOGICAL_OPERATORS.NOT) {
|
|
426
|
+
const innerExpr = i + 1 < children.length ? buildAST(children[i + 1]) : null;
|
|
427
|
+
return innerExpr ? {
|
|
428
|
+
type: "LogicalExpression",
|
|
429
|
+
operator: LOGICAL_OPERATORS.NOT,
|
|
430
|
+
left: innerExpr,
|
|
431
|
+
right: void 0
|
|
432
|
+
} : null;
|
|
433
|
+
}
|
|
434
|
+
for (let i = 0; i < children.length; i++) {
|
|
435
|
+
const result = buildAST(children[i]);
|
|
436
|
+
if (result) return result;
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* 构建逻辑表达式 AST(OR/AND 通用)
|
|
442
|
+
*/
|
|
443
|
+
function buildLogicalClauseAST(node, operator) {
|
|
444
|
+
const children = getChildren(node);
|
|
445
|
+
if (children.length === 1) return buildAST(children[0]);
|
|
446
|
+
if (children.length < 3) return null;
|
|
447
|
+
const opIndices = [];
|
|
448
|
+
for (let i = 0; i < children.length; i++) if (children[i] instanceof TerminalNode && children[i].text.toUpperCase() === operator) opIndices.push(i);
|
|
449
|
+
if (opIndices.length === 0) return buildAST(children[0]);
|
|
450
|
+
const lastOpIndex = opIndices[opIndices.length - 1];
|
|
451
|
+
let result = buildAST(children[lastOpIndex + 1]);
|
|
452
|
+
for (let i = opIndices.length - 1; i >= 0; i--) {
|
|
453
|
+
const leftNode = buildAST(children[i === 0 ? 0 : opIndices[i - 1] + 1]);
|
|
454
|
+
if (leftNode && result) result = {
|
|
455
|
+
type: "LogicalExpression",
|
|
456
|
+
operator,
|
|
457
|
+
left: leftNode,
|
|
458
|
+
right: result
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* 构建 OR 子句 AST
|
|
465
|
+
*/
|
|
466
|
+
function buildOrClauseAST(node) {
|
|
467
|
+
return buildLogicalClauseAST(node, LOGICAL_OPERATORS.OR);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* 构建 AND 子句 AST
|
|
471
|
+
*/
|
|
472
|
+
function buildAndClauseAST(node) {
|
|
473
|
+
return buildLogicalClauseAST(node, LOGICAL_OPERATORS.AND);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 构建列表 AST
|
|
477
|
+
*/
|
|
478
|
+
function buildListAST(node) {
|
|
479
|
+
const children = getChildren(node);
|
|
480
|
+
const elements = [];
|
|
481
|
+
for (let i = 0; i < children.length; i++) {
|
|
482
|
+
const child = children[i];
|
|
483
|
+
if (child instanceof TerminalNode && SKIP_TERMINALS.has(child.text)) continue;
|
|
484
|
+
if (getNodeInfo(child).ruleName === JQL_CONTEXT.OPERAND) {
|
|
485
|
+
const operandValue = buildAST(child);
|
|
486
|
+
if (operandValue) elements.push(operandValue);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
type: "List",
|
|
491
|
+
elements
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* 构建函数 AST
|
|
496
|
+
*/
|
|
497
|
+
function buildFunctionAST(node) {
|
|
498
|
+
const children = getChildren(node);
|
|
499
|
+
let functionName = null;
|
|
500
|
+
const args = [];
|
|
501
|
+
for (let i = 0; i < children.length; i++) {
|
|
502
|
+
const info = getNodeInfo(children[i]);
|
|
503
|
+
if (info.ruleName === JQL_CONTEXT.FUNCTION_NAME) functionName = extractFieldValue(children[i]);
|
|
504
|
+
else if (info.ruleName === JQL_CONTEXT.ARGUMENT_LIST) {
|
|
505
|
+
const argChildren = getChildren(children[i]);
|
|
506
|
+
for (let j = 0; j < argChildren.length; j++) if (getNodeInfo(argChildren[j]).ruleName === JQL_CONTEXT.ARGUMENT) {
|
|
507
|
+
const argValue = extractFieldValue(argChildren[j]);
|
|
508
|
+
if (argValue) args.push({
|
|
509
|
+
type: "Literal",
|
|
510
|
+
value: argValue
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return functionName ? {
|
|
516
|
+
type: "Function",
|
|
517
|
+
name: functionName,
|
|
518
|
+
arguments: args
|
|
519
|
+
} : null;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 构建查询 AST(包含 WHERE 和 ORDER BY)
|
|
523
|
+
*/
|
|
524
|
+
function buildQueryAST(node) {
|
|
525
|
+
const children = getChildren(node);
|
|
526
|
+
let whereClause = null;
|
|
527
|
+
let orderByClause = null;
|
|
528
|
+
for (const child of children) {
|
|
529
|
+
const childInfo = getNodeInfo(child);
|
|
530
|
+
if (childInfo.ruleName === JQL_CONTEXT.WHERE) whereClause = buildAST(child);
|
|
531
|
+
else if (childInfo.ruleName === JQL_CONTEXT.ORDER_BY) orderByClause = buildOrderByAST(child);
|
|
532
|
+
}
|
|
533
|
+
if (!orderByClause) return whereClause;
|
|
534
|
+
return {
|
|
535
|
+
type: "JQLQuery",
|
|
536
|
+
where: whereClause,
|
|
537
|
+
orderBy: orderByClause
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 构建 ORDER BY 子句的 AST
|
|
542
|
+
*/
|
|
543
|
+
function buildOrderByAST(node) {
|
|
544
|
+
if (getNodeInfo(node).ruleName !== JQL_CONTEXT.ORDER_BY) return null;
|
|
545
|
+
const children = getChildren(node);
|
|
546
|
+
const fields = [];
|
|
547
|
+
for (let i = 0; i < children.length; i++) if (getNodeInfo(children[i]).ruleName === JQL_CONTEXT.SEARCH_SORT) {
|
|
548
|
+
const sortChildren = getChildren(children[i]);
|
|
549
|
+
let direction = "ASC";
|
|
550
|
+
let fieldText = "";
|
|
551
|
+
for (let j = 0; j < sortChildren.length; j++) {
|
|
552
|
+
const sortChild = sortChildren[j];
|
|
553
|
+
if (sortChild instanceof TerminalNode) {
|
|
554
|
+
const text = sortChild.text.toUpperCase();
|
|
555
|
+
if (text === "ASC" || text === "DESC") {
|
|
556
|
+
direction = text;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
fieldText += sortChild.text;
|
|
560
|
+
} else fieldText += sortChild.text;
|
|
561
|
+
}
|
|
562
|
+
const fieldName = fieldText.trim();
|
|
563
|
+
if (fieldName) fields.push({
|
|
564
|
+
type: "OrderByField",
|
|
565
|
+
field: fieldName,
|
|
566
|
+
direction
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return fields.length > 0 ? {
|
|
570
|
+
type: "OrderByClause",
|
|
571
|
+
fields
|
|
572
|
+
} : null;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 提取复杂值(可能是列表、函数或简单值)
|
|
576
|
+
*/
|
|
577
|
+
function extractComplexValue(node, operator) {
|
|
578
|
+
if (!node) return null;
|
|
579
|
+
const priorityContexts = [];
|
|
580
|
+
if (operator === NULL_OPERATORS.IS || operator === NULL_OPERATORS.IS_NOT) priorityContexts.push(JQL_CONTEXT.EMPTY);
|
|
581
|
+
else if (operator === SET_OPERATORS.IN || operator === SET_OPERATORS.NOT_IN) priorityContexts.push(JQL_CONTEXT.LIST, JQL_CONTEXT.FUNCTION);
|
|
582
|
+
else priorityContexts.push(JQL_CONTEXT.FUNCTION, JQL_CONTEXT.VALUE);
|
|
583
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
584
|
+
const child = node.getChild(i);
|
|
585
|
+
const childInfo = getNodeInfo(child);
|
|
586
|
+
if (priorityContexts.includes(childInfo.ruleName || "")) {
|
|
587
|
+
const result = buildAST(child);
|
|
588
|
+
if (result) return result;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
592
|
+
const result = extractComplexValue(node.getChild(i), operator);
|
|
593
|
+
if (result) return result;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/ast-to-iql/field-converters.ts
|
|
600
|
+
/**
|
|
601
|
+
* 规范化字段名,将 cf[xxx] 格式转换为 xxx
|
|
602
|
+
* @param fieldName 原始字段名
|
|
603
|
+
* @returns 规范化后的字段名
|
|
604
|
+
*/
|
|
605
|
+
function normalizeFieldName(fieldName) {
|
|
606
|
+
const match = fieldName.match(/^cf\[(\d+)\]$/);
|
|
607
|
+
if (match) return match[1];
|
|
608
|
+
return fieldName;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* 过滤 List 中的函数节点(IQL 不支持的函数)
|
|
612
|
+
* @param elements List 中的元素数组
|
|
613
|
+
* @returns 过滤后的元素数组
|
|
614
|
+
*/
|
|
615
|
+
function filterFunctions(elements) {
|
|
616
|
+
return elements.filter((el) => el.type !== "Function");
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* 清理值中的引号包裹
|
|
620
|
+
* @param value 原始值
|
|
621
|
+
* @returns 清理后的值
|
|
622
|
+
*/
|
|
623
|
+
function cleanQuotes(value) {
|
|
624
|
+
if (value.startsWith("\"") && value.endsWith("\"") && value.length > 1) return value.slice(1, -1);
|
|
625
|
+
return value;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 通用的字段值转换函数
|
|
629
|
+
* @param value AST 节点
|
|
630
|
+
* @param config 转换配置
|
|
631
|
+
* @returns 转换后的字符串值
|
|
632
|
+
*/
|
|
633
|
+
function convertValue(value, config) {
|
|
634
|
+
if (value.type === AST_NODE_TYPES.FUNCTION) {
|
|
635
|
+
if (!config.supportsFunctions) return "";
|
|
636
|
+
const args = value.arguments || [];
|
|
637
|
+
const getArgValue = (arg) => {
|
|
638
|
+
return cleanQuotes(arg.type === AST_NODE_TYPES.LITERAL ? String(arg.value) : String(arg));
|
|
639
|
+
};
|
|
640
|
+
if (value.name === FUNCTION_NAMES.MEMBERS_OF) {
|
|
641
|
+
if (args.length > 0) return `${value.name}(${getArgValue(args[0])})`;
|
|
642
|
+
return `${value.name}()`;
|
|
643
|
+
}
|
|
644
|
+
if (value.name === FUNCTION_NAMES.CASCADE_OPTION && args.length > 0) {
|
|
645
|
+
var _config$valueMap;
|
|
646
|
+
const argValue = getArgValue(args[0]);
|
|
647
|
+
if ((_config$valueMap = config.valueMap) === null || _config$valueMap === void 0 ? void 0 : _config$valueMap[argValue]) return `"${config.valueMap[argValue]}"`;
|
|
648
|
+
return `"${argValue}"`;
|
|
649
|
+
}
|
|
650
|
+
return `"${value.name}()"`;
|
|
651
|
+
}
|
|
652
|
+
if (value.type === AST_NODE_TYPES.LITERAL) {
|
|
653
|
+
var _config$valueMap2;
|
|
654
|
+
let val = cleanQuotes(String(value.value));
|
|
655
|
+
if ((_config$valueMap2 = config.valueMap) === null || _config$valueMap2 === void 0 ? void 0 : _config$valueMap2[val]) {
|
|
656
|
+
const mappedValue = config.valueMap[val];
|
|
657
|
+
val = Array.isArray(mappedValue) ? mappedValue[0] : mappedValue;
|
|
658
|
+
}
|
|
659
|
+
if (config.isNumeric) {
|
|
660
|
+
const numValue = parseFloat(val);
|
|
661
|
+
if (!isNaN(numValue) && numValue < 0) return "";
|
|
662
|
+
return val;
|
|
663
|
+
}
|
|
664
|
+
return `"${val}"`;
|
|
665
|
+
}
|
|
666
|
+
return "";
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* 通用的等值操作符处理(= 和 !=)
|
|
670
|
+
* @param operator 操作符
|
|
671
|
+
* @param value 值节点
|
|
672
|
+
* @param config 转换配置
|
|
673
|
+
* @returns IQL 查询字符串
|
|
674
|
+
*/
|
|
675
|
+
function handleEqualityOperator(operator, value, config) {
|
|
676
|
+
const normalizedOp = operator.toLowerCase();
|
|
677
|
+
if (normalizedOp !== COMPARISON_OPERATORS.EQUAL && normalizedOp !== COMPARISON_OPERATORS.NOT_EQUAL) return "";
|
|
678
|
+
if (value.type === AST_NODE_TYPES.LITERAL) {
|
|
679
|
+
const val = String(value.value);
|
|
680
|
+
if (config.handleEmpty && val.toUpperCase() === NULL_KEYWORDS.EMPTY) {
|
|
681
|
+
const nullOp = normalizedOp === "=" ? "is" : "is not";
|
|
682
|
+
const nullKeyword = config.nullKeyword || "null";
|
|
683
|
+
return `"${config.iqlFieldName}" ${nullOp} ${nullKeyword}`;
|
|
684
|
+
}
|
|
685
|
+
if (config.useInForEquality && config.valueMap) {
|
|
686
|
+
const cleanVal = cleanQuotes(val);
|
|
687
|
+
const mappedValue = config.valueMap[cleanVal];
|
|
688
|
+
const valuesStr = (mappedValue ? Array.isArray(mappedValue) ? mappedValue : [mappedValue] : [cleanVal]).map((v) => `"${v}"`).join(", ");
|
|
689
|
+
const inOp = normalizedOp === "=" ? "in" : "not in";
|
|
690
|
+
return `"${config.iqlFieldName}" ${inOp} [${valuesStr}]`;
|
|
691
|
+
}
|
|
692
|
+
const convertedValue = convertValue(value, config);
|
|
693
|
+
if (convertedValue) return `"${config.iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
694
|
+
}
|
|
695
|
+
if (value.type === AST_NODE_TYPES.FUNCTION && config.supportsFunctions) {
|
|
696
|
+
const convertedValue = convertValue(value, config);
|
|
697
|
+
return `"${config.iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
698
|
+
}
|
|
699
|
+
return "";
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* 通用的比较操作符处理(>, >=, <, <=)
|
|
703
|
+
* @param operator 操作符
|
|
704
|
+
* @param value 值节点
|
|
705
|
+
* @param config 转换配置
|
|
706
|
+
* @returns IQL 查询字符串
|
|
707
|
+
*/
|
|
708
|
+
function handleComparisonOperator(operator, value, config) {
|
|
709
|
+
const normalizedOp = operator.toLowerCase();
|
|
710
|
+
if (![
|
|
711
|
+
COMPARISON_OPERATORS.GREATER_THAN,
|
|
712
|
+
COMPARISON_OPERATORS.GREATER_THAN_OR_EQUAL,
|
|
713
|
+
COMPARISON_OPERATORS.LESS_THAN,
|
|
714
|
+
COMPARISON_OPERATORS.LESS_THAN_OR_EQUAL
|
|
715
|
+
].includes(normalizedOp)) return "";
|
|
716
|
+
if (value.type === AST_NODE_TYPES.LITERAL) {
|
|
717
|
+
const convertedValue = convertValue(value, config);
|
|
718
|
+
if (convertedValue) return `"${config.iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
719
|
+
}
|
|
720
|
+
return "";
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* 通用的 IN/NOT IN 操作符处理
|
|
724
|
+
* @param operator 操作符
|
|
725
|
+
* @param value 值节点(应该是 List 类型或 Function 类型)
|
|
726
|
+
* @param config 转换配置
|
|
727
|
+
* @returns IQL 查询字符串
|
|
728
|
+
*/
|
|
729
|
+
function handleInOperator(operator, value, config) {
|
|
730
|
+
const normalizedOp = operator.toLowerCase();
|
|
731
|
+
if (normalizedOp !== SET_OPERATORS.IN && normalizedOp !== SET_OPERATORS.NOT_IN) return "";
|
|
732
|
+
if (value.type === AST_NODE_TYPES.FUNCTION && config.supportsFunctions) {
|
|
733
|
+
const convertedValue = convertValue(value, config);
|
|
734
|
+
if (convertedValue) return `"${config.iqlFieldName}" ${normalizedOp} [${convertedValue}]`;
|
|
735
|
+
}
|
|
736
|
+
if (value.type === AST_NODE_TYPES.LIST) {
|
|
737
|
+
const emptyElements = [];
|
|
738
|
+
const nonEmptyElements = [];
|
|
739
|
+
for (const el of value.elements) {
|
|
740
|
+
if (!config.supportsFunctions && el.type === AST_NODE_TYPES.FUNCTION) continue;
|
|
741
|
+
if (config.handleEmpty && el.type === AST_NODE_TYPES.LITERAL && /^empty$/i.test(String(el.value))) emptyElements.push(el);
|
|
742
|
+
else nonEmptyElements.push(el);
|
|
743
|
+
}
|
|
744
|
+
const conditions = [];
|
|
745
|
+
if (nonEmptyElements.length > 0) {
|
|
746
|
+
const allValues = [];
|
|
747
|
+
for (const el of nonEmptyElements) if (el.type === AST_NODE_TYPES.LITERAL) {
|
|
748
|
+
var _config$valueMap3;
|
|
749
|
+
const cleanVal = cleanQuotes(String(el.value));
|
|
750
|
+
const mappedValue = (_config$valueMap3 = config.valueMap) === null || _config$valueMap3 === void 0 ? void 0 : _config$valueMap3[cleanVal];
|
|
751
|
+
if (mappedValue) {
|
|
752
|
+
const values = Array.isArray(mappedValue) ? mappedValue : [mappedValue];
|
|
753
|
+
allValues.push(...values.map((v) => `"${v}"`));
|
|
754
|
+
} else {
|
|
755
|
+
const convertedValue = convertValue(el, config);
|
|
756
|
+
if (convertedValue) allValues.push(convertedValue);
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
const convertedValue = convertValue(el, config);
|
|
760
|
+
if (convertedValue) allValues.push(convertedValue);
|
|
761
|
+
}
|
|
762
|
+
if (allValues.length > 0) conditions.push(`"${config.iqlFieldName}" ${normalizedOp} [${allValues.join(", ")}]`);
|
|
763
|
+
}
|
|
764
|
+
if (emptyElements.length > 0) {
|
|
765
|
+
const nullOp = normalizedOp === SET_OPERATORS.IN ? NULL_OPERATORS.IS : NULL_OPERATORS.IS_NOT;
|
|
766
|
+
const nullKeyword = config.nullKeyword || NULL_KEYWORDS.NULL;
|
|
767
|
+
conditions.push(`"${config.iqlFieldName}" ${nullOp} ${nullKeyword}`);
|
|
768
|
+
}
|
|
769
|
+
if (conditions.length > 1) {
|
|
770
|
+
const connector = normalizedOp === SET_OPERATORS.IN ? IQL_LOGICAL_OPERATORS.OR : IQL_LOGICAL_OPERATORS.AND;
|
|
771
|
+
return `(${conditions.join(` ${connector} `)})`;
|
|
772
|
+
} else if (conditions.length === 1) return conditions[0];
|
|
773
|
+
}
|
|
774
|
+
return "";
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* 通用的 IS/IS NOT 操作符处理
|
|
778
|
+
* @param operator 操作符
|
|
779
|
+
* @param value 值节点
|
|
780
|
+
* @param config 转换配置
|
|
781
|
+
* @returns IQL 查询字符串
|
|
782
|
+
*/
|
|
783
|
+
function handleIsOperator(operator, value, config) {
|
|
784
|
+
const normalizedOp = operator.toLowerCase();
|
|
785
|
+
if ((normalizedOp === "is" || normalizedOp === "is not") && value.type === "Literal") {
|
|
786
|
+
const val = String(value.value).toUpperCase();
|
|
787
|
+
if (val === "NULL" || val === "EMPTY") {
|
|
788
|
+
const nullKeyword = config.nullKeyword || "null";
|
|
789
|
+
return `"${config.iqlFieldName}" ${normalizedOp} ${nullKeyword}`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return "";
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* 通用的模糊匹配操作符处理(~ 和 !~)
|
|
796
|
+
* @param operator 操作符
|
|
797
|
+
* @param value 值节点
|
|
798
|
+
* @param config 转换配置
|
|
799
|
+
* @returns IQL 查询字符串
|
|
800
|
+
*/
|
|
801
|
+
function handleContainsOperator(operator, value, config) {
|
|
802
|
+
const normalizedOp = operator.toLowerCase();
|
|
803
|
+
if ((normalizedOp === "~" || normalizedOp === "!~") && value.type === "Literal") {
|
|
804
|
+
const convertedValue = convertValue(value, config);
|
|
805
|
+
if (convertedValue) return `"${config.iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
806
|
+
}
|
|
807
|
+
return "";
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* 通用字段转换器
|
|
811
|
+
* @param fieldName 字段名
|
|
812
|
+
* @param operator 操作符
|
|
813
|
+
* @param value 值节点
|
|
814
|
+
* @param constantsMap 常量映射
|
|
815
|
+
* @param options 额外选项
|
|
816
|
+
* @returns IQL 查询字符串
|
|
817
|
+
*/
|
|
818
|
+
function convertGenericField(fieldName, operator, value, constantsMap, options = {}) {
|
|
819
|
+
const fieldConfig = (constantsMap.fieldMap || {})[normalizeFieldName(fieldName)];
|
|
820
|
+
const config = {
|
|
821
|
+
iqlFieldName: (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.customFieldKey) || fieldName,
|
|
822
|
+
supportsFunctions: options.supportsFunctions || false,
|
|
823
|
+
valueMap: options.valueMapKey ? constantsMap[options.valueMapKey] : void 0,
|
|
824
|
+
isNumeric: options.isNumeric || false,
|
|
825
|
+
handleEmpty: options.handleEmpty || false,
|
|
826
|
+
nullKeyword: options.nullKeyword || "null",
|
|
827
|
+
useInForEquality: options.useInForEquality || false
|
|
828
|
+
};
|
|
829
|
+
const normalizedOp = operator.toLowerCase();
|
|
830
|
+
if (normalizedOp === "=" || normalizedOp === "!=") return handleEqualityOperator(operator, value, config);
|
|
831
|
+
if ([
|
|
832
|
+
">",
|
|
833
|
+
">=",
|
|
834
|
+
"<",
|
|
835
|
+
"<="
|
|
836
|
+
].includes(normalizedOp)) return handleComparisonOperator(operator, value, config);
|
|
837
|
+
if (normalizedOp === "in" || normalizedOp === "not in") return handleInOperator(operator, value, config);
|
|
838
|
+
if (normalizedOp === "is" || normalizedOp === "is not") return handleIsOperator(operator, value, config);
|
|
839
|
+
if (normalizedOp === "~" || normalizedOp === "!~") return handleContainsOperator(operator, value, config);
|
|
840
|
+
return "";
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* 转换 project 字段
|
|
844
|
+
*/
|
|
845
|
+
function convertProjectField(operator, value, constantsMap) {
|
|
846
|
+
const projectMap = constantsMap.projectMap || {};
|
|
847
|
+
if (operator === "=") {
|
|
848
|
+
if (value.type === "Literal") {
|
|
849
|
+
const projectKey = String(value.value);
|
|
850
|
+
return `"所属空间" = "${projectMap[projectKey] || projectKey}"`;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (operator === "in") {
|
|
854
|
+
if (value.type === "List") return `"所属空间" in [${filterFunctions(value.elements).filter((el) => el.type === "Literal").map((el) => {
|
|
855
|
+
const projectKey = String(el.value);
|
|
856
|
+
return projectMap[projectKey] || projectKey;
|
|
857
|
+
}).map((name) => `"${name}"`).join(", ")}]`;
|
|
858
|
+
}
|
|
859
|
+
return "";
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* 转换 issuetype 字段
|
|
863
|
+
*/
|
|
864
|
+
function convertIssuetypeField(operator, value, constantsMap) {
|
|
865
|
+
if (operator === "=") {
|
|
866
|
+
if (value.type === "Literal") return `"类型" = "${String(value.value)}"`;
|
|
867
|
+
}
|
|
868
|
+
if (operator === "in") {
|
|
869
|
+
if (value.type === "List") return `"类型" in [${filterFunctions(value.elements).filter((el) => el.type === "Literal").map((el) => `"${String(el.value)}"`).join(",")}]`;
|
|
870
|
+
}
|
|
871
|
+
return "";
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* 转换用户字段
|
|
875
|
+
* @param fieldName JQL 字段名
|
|
876
|
+
* @param operator 操作符
|
|
877
|
+
* @param value 值
|
|
878
|
+
* @param constantsMap 常量映射,包含 fieldMap 配置
|
|
879
|
+
*/
|
|
880
|
+
function convertUserField(fieldName, operator, value, constantsMap) {
|
|
881
|
+
return convertGenericField(fieldName, operator, value, constantsMap, {
|
|
882
|
+
supportsFunctions: true,
|
|
883
|
+
handleEmpty: true
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* 转换版本字段
|
|
888
|
+
* @param fieldName JQL 字段名
|
|
889
|
+
* @param operator 操作符
|
|
890
|
+
* @param value 值
|
|
891
|
+
* @param constantsMap 常量映射
|
|
892
|
+
*/
|
|
893
|
+
function convertVersionField(fieldName, operator, value, constantsMap) {
|
|
894
|
+
return convertGenericField(fieldName, operator, value, constantsMap, {
|
|
895
|
+
valueMapKey: "versionMap",
|
|
896
|
+
handleEmpty: true,
|
|
897
|
+
nullKeyword: "NULL",
|
|
898
|
+
useInForEquality: true
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* 转换 component 字段(Tree 类型)
|
|
903
|
+
* @param operator 操作符
|
|
904
|
+
* @param value 值
|
|
905
|
+
* @param constantsMap 常量映射
|
|
906
|
+
*/
|
|
907
|
+
function convertComponentField(operator, value, constantsMap) {
|
|
908
|
+
return convertGenericField("所属模块", operator, value, constantsMap, {
|
|
909
|
+
valueMapKey: "component",
|
|
910
|
+
handleEmpty: true,
|
|
911
|
+
nullKeyword: "NULL"
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* 转换 Cascade 字段(级联选择)
|
|
916
|
+
* @param fieldName JQL 字段名
|
|
917
|
+
* @param operator 操作符
|
|
918
|
+
* @param value 值
|
|
919
|
+
* @param constantsMap 常量映射
|
|
920
|
+
*/
|
|
921
|
+
function convertCascadeField(fieldName, operator, value, constantsMap) {
|
|
922
|
+
return convertGenericField(fieldName, operator, value, constantsMap, {
|
|
923
|
+
supportsFunctions: true,
|
|
924
|
+
valueMapKey: "cascade",
|
|
925
|
+
handleEmpty: true,
|
|
926
|
+
nullKeyword: "null"
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* 转换状态字段
|
|
931
|
+
* @param operator 操作符
|
|
932
|
+
* @param value 值
|
|
933
|
+
* @param constantsMap 常量映射
|
|
934
|
+
*/
|
|
935
|
+
function convertStatusField(operator, value, constantsMap) {
|
|
936
|
+
if (operator === "=") {
|
|
937
|
+
if (value.type === "Literal") return `"状态" = "${String(value.value).replace(/^"|"$/g, "")}"`;
|
|
938
|
+
}
|
|
939
|
+
if (operator === "in") {
|
|
940
|
+
if (value.type === "List") return `"状态" in [${filterFunctions(value.elements).filter((el) => el.type === "Literal").map((el) => {
|
|
941
|
+
return `"${String(el.value).replace(/^"|"$/g, "")}"`;
|
|
942
|
+
}).join(", ")}]`;
|
|
943
|
+
}
|
|
944
|
+
return "";
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* 转换 Sprint 字段
|
|
948
|
+
* @param operator 操作符
|
|
949
|
+
* @param value 值
|
|
950
|
+
* @param constantsMap 常量映射
|
|
951
|
+
*/
|
|
952
|
+
function convertSprintField(operator, value, constantsMap) {
|
|
953
|
+
if (operator === "=") {
|
|
954
|
+
if (value.type === "Literal") return `"迭代" = "${String(value.value)}"`;
|
|
955
|
+
}
|
|
956
|
+
if (operator === "in") {
|
|
957
|
+
if (value.type === "List") return `"迭代" in [${filterFunctions(value.elements).filter((el) => el.type === "Literal").map((el) => {
|
|
958
|
+
return `"${String(el.value)}"`;
|
|
959
|
+
}).join(", ")}]`;
|
|
960
|
+
}
|
|
961
|
+
return "";
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* 转换 Priority 字段
|
|
965
|
+
* @param operator 操作符
|
|
966
|
+
* @param value 值
|
|
967
|
+
* @param constantsMap 常量映射
|
|
968
|
+
*/
|
|
969
|
+
function convertPriorityField(operator, value, constantsMap) {
|
|
970
|
+
return convertGenericField("优先级", operator, value, constantsMap, { handleEmpty: false });
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* 转换枚举字段(Checkbox/Dropdown/Radio)
|
|
974
|
+
* @param fieldName JQL 字段名
|
|
975
|
+
* @param operator 操作符
|
|
976
|
+
* @param value 值
|
|
977
|
+
* @param constantsMap 常量映射
|
|
978
|
+
*/
|
|
979
|
+
function convertEnumField(fieldName, operator, value, constantsMap) {
|
|
980
|
+
const fieldConfig = (constantsMap.fieldMap || {})[normalizeFieldName(fieldName)];
|
|
981
|
+
return convertGenericField(fieldName, operator, value, constantsMap, {
|
|
982
|
+
handleEmpty: false,
|
|
983
|
+
valueMapKey: (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.customFieldKey) || fieldName
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* 转换 Text/LongText/Name 字段
|
|
988
|
+
* @param fieldName JQL 字段名
|
|
989
|
+
* @param operator 操作符
|
|
990
|
+
* @param value 值
|
|
991
|
+
* @param constantsMap 常量映射
|
|
992
|
+
*/
|
|
993
|
+
function convertTextField(fieldName, operator, value, constantsMap) {
|
|
994
|
+
return convertGenericField(fieldName, operator, value, constantsMap, { handleEmpty: false });
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* 转换数值字段 (Number/Integer/Float)
|
|
998
|
+
* @param fieldName JQL 字段名
|
|
999
|
+
* @param operator 操作符
|
|
1000
|
+
* @param value 值
|
|
1001
|
+
* @param constantsMap 常量映射
|
|
1002
|
+
*/
|
|
1003
|
+
function convertNumberField(fieldName, operator, value, constantsMap) {
|
|
1004
|
+
return convertGenericField(fieldName, operator, value, constantsMap, {
|
|
1005
|
+
isNumeric: true,
|
|
1006
|
+
handleEmpty: true,
|
|
1007
|
+
nullKeyword: "null"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* 转换用户组字段值(使用单引号)
|
|
1012
|
+
* @param value AST 节点
|
|
1013
|
+
* @returns 转换后的字符串值
|
|
1014
|
+
*/
|
|
1015
|
+
function convertUserGroupValue(value) {
|
|
1016
|
+
if (value.type === "Literal") return `'${cleanQuotes(String(value.value))}'`;
|
|
1017
|
+
return "";
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* 转换用户组字段 (UserGroup)
|
|
1021
|
+
* @param fieldName JQL 字段名
|
|
1022
|
+
* @param operator 操作符
|
|
1023
|
+
* @param value 值
|
|
1024
|
+
* @param constantsMap 常量映射
|
|
1025
|
+
*/
|
|
1026
|
+
function convertUserGroupField(fieldName, operator, value, constantsMap) {
|
|
1027
|
+
const fieldConfig = (constantsMap.fieldMap || {})[normalizeFieldName(fieldName)];
|
|
1028
|
+
const iqlFieldName = (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.customFieldKey) || fieldName;
|
|
1029
|
+
const normalizedOp = operator.toLowerCase();
|
|
1030
|
+
if (normalizedOp === "=" || normalizedOp === "!=") {
|
|
1031
|
+
const convertedValue = convertUserGroupValue(value);
|
|
1032
|
+
if (convertedValue) return `"${iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
1033
|
+
return "";
|
|
1034
|
+
}
|
|
1035
|
+
if (normalizedOp === "in" || normalizedOp === "not in") {
|
|
1036
|
+
if (value.type === "List") {
|
|
1037
|
+
const conditions = [];
|
|
1038
|
+
const values = [];
|
|
1039
|
+
let hasEmpty = false;
|
|
1040
|
+
for (const el of value.elements) if (el.type === "Literal" && String(el.value).toUpperCase() === "EMPTY") hasEmpty = true;
|
|
1041
|
+
else {
|
|
1042
|
+
const convertedValue = convertUserGroupValue(el);
|
|
1043
|
+
if (convertedValue) values.push(convertedValue);
|
|
1044
|
+
}
|
|
1045
|
+
if (values.length > 0) conditions.push(`"${iqlFieldName}" ${normalizedOp} [${values.join(", ")}]`);
|
|
1046
|
+
if (hasEmpty) {
|
|
1047
|
+
const nullOp = normalizedOp === "in" ? "is" : "is not";
|
|
1048
|
+
conditions.push(`"${iqlFieldName}" ${nullOp} null`);
|
|
1049
|
+
}
|
|
1050
|
+
if (conditions.length > 1) {
|
|
1051
|
+
const connector = normalizedOp === "in" ? "or" : "and";
|
|
1052
|
+
return `(${conditions.join(` ${connector} `)})`;
|
|
1053
|
+
} else if (conditions.length === 1) return conditions[0];
|
|
1054
|
+
}
|
|
1055
|
+
return "";
|
|
1056
|
+
}
|
|
1057
|
+
if (normalizedOp === "is" || normalizedOp === "is not") {
|
|
1058
|
+
if (value.type === "Literal") {
|
|
1059
|
+
const val = String(value.value).toUpperCase();
|
|
1060
|
+
if (val === "NULL" || val === "EMPTY") return `"${iqlFieldName}" ${normalizedOp} null`;
|
|
1061
|
+
}
|
|
1062
|
+
return "";
|
|
1063
|
+
}
|
|
1064
|
+
return "";
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* IQL 支持的日期函数列表
|
|
1068
|
+
*/
|
|
1069
|
+
const SUPPORTED_DATE_FUNCTIONS = [
|
|
1070
|
+
"startOfDay",
|
|
1071
|
+
"endOfDay",
|
|
1072
|
+
"startOfWeek",
|
|
1073
|
+
"endOfWeek",
|
|
1074
|
+
"startOfMonth",
|
|
1075
|
+
"endOfMonth",
|
|
1076
|
+
"startOfYear",
|
|
1077
|
+
"endOfYear"
|
|
1078
|
+
];
|
|
1079
|
+
/**
|
|
1080
|
+
* 转换日期函数节点
|
|
1081
|
+
* @param funcNode 函数节点
|
|
1082
|
+
* @returns 转换后的函数字符串,不支持的函数返回空字符串
|
|
1083
|
+
*/
|
|
1084
|
+
function convertDateFunction(funcNode) {
|
|
1085
|
+
if (funcNode.type !== "Function") return "";
|
|
1086
|
+
const funcName = funcNode.name;
|
|
1087
|
+
if (!SUPPORTED_DATE_FUNCTIONS.includes(funcName)) return "";
|
|
1088
|
+
const args = funcNode.arguments || [];
|
|
1089
|
+
if (args.length === 0) return `${funcName}()`;
|
|
1090
|
+
else if (args.length === 1) {
|
|
1091
|
+
const arg = args[0];
|
|
1092
|
+
return `${funcName}(${typeof arg === "object" && arg !== null && "value" in arg ? arg.value : arg})`;
|
|
1093
|
+
}
|
|
1094
|
+
return "";
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* 转换日期字段值
|
|
1098
|
+
* @param value AST 节点
|
|
1099
|
+
* @returns 转换后的字符串值
|
|
1100
|
+
*/
|
|
1101
|
+
function convertDateValue(value) {
|
|
1102
|
+
if (value.type === "Function") return convertDateFunction(value);
|
|
1103
|
+
if (value.type === "Literal") {
|
|
1104
|
+
const cleanVal = cleanQuotes(String(value.value));
|
|
1105
|
+
const timeUnitMatch = cleanVal.match(/^([+-]?\d+)([mhdw])$/i);
|
|
1106
|
+
if (timeUnitMatch) {
|
|
1107
|
+
const num = parseInt(timeUnitMatch[1], 10);
|
|
1108
|
+
const unit = timeUnitMatch[2].toLowerCase();
|
|
1109
|
+
if (unit === "m" || unit === "h") return "";
|
|
1110
|
+
if (unit === "d") return num === 0 ? "startOfDay()" : `startOfDay(${num})`;
|
|
1111
|
+
if (unit === "w") {
|
|
1112
|
+
const days = num * 7;
|
|
1113
|
+
return days === 0 ? "startOfDay()" : `startOfDay(${days})`;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return `"${cleanVal}"`;
|
|
1117
|
+
}
|
|
1118
|
+
return "";
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* 转换日期字段 (Date/createdAt/updatedAt)
|
|
1122
|
+
* @param fieldName JQL 字段名
|
|
1123
|
+
* @param operator 操作符
|
|
1124
|
+
* @param value 值
|
|
1125
|
+
* @param constantsMap 常量映射
|
|
1126
|
+
*/
|
|
1127
|
+
function convertDateField(fieldName, operator, value, constantsMap) {
|
|
1128
|
+
const fieldConfig = (constantsMap.fieldMap || {})[normalizeFieldName(fieldName)];
|
|
1129
|
+
const iqlFieldName = (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.customFieldKey) || fieldName;
|
|
1130
|
+
const normalizedOp = operator.toLowerCase();
|
|
1131
|
+
if (normalizedOp === "=" || normalizedOp === "!=") {
|
|
1132
|
+
const convertedValue = convertDateValue(value);
|
|
1133
|
+
if (convertedValue) return `"${iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
1134
|
+
return "";
|
|
1135
|
+
}
|
|
1136
|
+
if ([
|
|
1137
|
+
">",
|
|
1138
|
+
">=",
|
|
1139
|
+
"<",
|
|
1140
|
+
"<="
|
|
1141
|
+
].includes(normalizedOp)) {
|
|
1142
|
+
const convertedValue = convertDateValue(value);
|
|
1143
|
+
if (convertedValue) return `"${iqlFieldName}" ${normalizedOp} ${convertedValue}`;
|
|
1144
|
+
return "";
|
|
1145
|
+
}
|
|
1146
|
+
if (normalizedOp === "in" || normalizedOp === "not in") {
|
|
1147
|
+
if (value.type === "List") {
|
|
1148
|
+
let hasEmpty = false;
|
|
1149
|
+
const values = [];
|
|
1150
|
+
for (const el of value.elements) if (el.type === "Literal" && String(el.value).toUpperCase() === "EMPTY") hasEmpty = true;
|
|
1151
|
+
else {
|
|
1152
|
+
const convertedValue = convertDateValue(el);
|
|
1153
|
+
if (convertedValue) values.push(convertedValue);
|
|
1154
|
+
}
|
|
1155
|
+
if (hasEmpty && values.length === 0) return `"${iqlFieldName}" ${normalizedOp === "in" ? "is" : "is not"} null`;
|
|
1156
|
+
if (hasEmpty && values.length > 0) {
|
|
1157
|
+
const nullOp = normalizedOp === "in" ? "is" : "is not";
|
|
1158
|
+
return `("${iqlFieldName}" ${normalizedOp} [${values.join(", ")}] or "${iqlFieldName}" ${nullOp} null)`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return "";
|
|
1162
|
+
}
|
|
1163
|
+
if (normalizedOp === "is" || normalizedOp === "is not") {
|
|
1164
|
+
if (value.type === "Literal") {
|
|
1165
|
+
const val = String(value.value).toUpperCase();
|
|
1166
|
+
if (val === "NULL" || val === "EMPTY") return `"${iqlFieldName}" ${normalizedOp} null`;
|
|
1167
|
+
}
|
|
1168
|
+
return "";
|
|
1169
|
+
}
|
|
1170
|
+
return "";
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/ast-to-iql/iql-converter.ts
|
|
1175
|
+
/**
|
|
1176
|
+
* 验证运算符是否合法
|
|
1177
|
+
* @param operator 运算符
|
|
1178
|
+
* @param rightNode 右侧节点
|
|
1179
|
+
* @returns 是否合法
|
|
1180
|
+
*/
|
|
1181
|
+
function isValidOperator(operator, rightNode) {
|
|
1182
|
+
const normalizedOp = operator.toLowerCase();
|
|
1183
|
+
const nodeType = rightNode === null || rightNode === void 0 ? void 0 : rightNode.type;
|
|
1184
|
+
const nodeValue = rightNode === null || rightNode === void 0 ? void 0 : rightNode.value;
|
|
1185
|
+
const isNull = nodeType === "Literal" && /^(null|empty)$/i.test(String(nodeValue));
|
|
1186
|
+
switch (normalizedOp) {
|
|
1187
|
+
case COMPARISON_OPERATORS.EQUAL:
|
|
1188
|
+
case COMPARISON_OPERATORS.NOT_EQUAL:
|
|
1189
|
+
case COMPARISON_OPERATORS.GREATER_THAN:
|
|
1190
|
+
case COMPARISON_OPERATORS.GREATER_THAN_OR_EQUAL:
|
|
1191
|
+
case COMPARISON_OPERATORS.LESS_THAN:
|
|
1192
|
+
case COMPARISON_OPERATORS.LESS_THAN_OR_EQUAL:
|
|
1193
|
+
case COMPARISON_OPERATORS.LIKE:
|
|
1194
|
+
case COMPARISON_OPERATORS.NOT_LIKE: return nodeType === AST_NODE_TYPES.LITERAL && !isNull || nodeType === AST_NODE_TYPES.FUNCTION;
|
|
1195
|
+
case SET_OPERATORS.IN:
|
|
1196
|
+
case SET_OPERATORS.NOT_IN: return nodeType === AST_NODE_TYPES.LIST || nodeType === AST_NODE_TYPES.FUNCTION;
|
|
1197
|
+
case NULL_OPERATORS.IS:
|
|
1198
|
+
case NULL_OPERATORS.IS_NOT: return isNull;
|
|
1199
|
+
default: return false;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* 将 AST 节点转换为 IQL 查询字符串
|
|
1204
|
+
*/
|
|
1205
|
+
function astToIQL(ast, constantsMap) {
|
|
1206
|
+
if (ast.type === AST_NODE_TYPES.BINARY_EXPRESSION) return convertBinaryExpression(ast, constantsMap);
|
|
1207
|
+
if (ast.type === AST_NODE_TYPES.LOGICAL_EXPRESSION) return convertLogicalExpression(ast, constantsMap);
|
|
1208
|
+
if (ast.type === AST_NODE_TYPES.JQL_QUERY) {
|
|
1209
|
+
let result = "";
|
|
1210
|
+
if (ast.where) result = astToIQL(ast.where, constantsMap);
|
|
1211
|
+
if (ast.orderBy) {
|
|
1212
|
+
const orderByClause = convertOrderByClause(ast.orderBy, constantsMap);
|
|
1213
|
+
if (orderByClause) result = result ? `${result} ${orderByClause}` : orderByClause;
|
|
1214
|
+
}
|
|
1215
|
+
return result;
|
|
1216
|
+
}
|
|
1217
|
+
return "";
|
|
1218
|
+
}
|
|
1219
|
+
const FIELD_TYPE_GROUPS = {
|
|
1220
|
+
user: new Set([
|
|
1221
|
+
"createdBy",
|
|
1222
|
+
"updatedBy",
|
|
1223
|
+
"Assignee",
|
|
1224
|
+
"Reporter",
|
|
1225
|
+
"User"
|
|
1226
|
+
]),
|
|
1227
|
+
version: new Set(["Version", "CustomVersion"]),
|
|
1228
|
+
text: new Set([
|
|
1229
|
+
"Text",
|
|
1230
|
+
"LongText",
|
|
1231
|
+
"Name",
|
|
1232
|
+
"Key"
|
|
1233
|
+
]),
|
|
1234
|
+
number: new Set(["Number", "StoryPoint"]),
|
|
1235
|
+
date: new Set([
|
|
1236
|
+
"Date",
|
|
1237
|
+
"createdAt",
|
|
1238
|
+
"updatedAt"
|
|
1239
|
+
]),
|
|
1240
|
+
enum: new Set([
|
|
1241
|
+
"Checkbox",
|
|
1242
|
+
"Dropdown",
|
|
1243
|
+
"Radio"
|
|
1244
|
+
])
|
|
1245
|
+
};
|
|
1246
|
+
const SPECIAL_FIELD_HANDLERS = new Map([
|
|
1247
|
+
["project", convertProjectField],
|
|
1248
|
+
["issuetype", convertIssuetypeField],
|
|
1249
|
+
["status", convertStatusField],
|
|
1250
|
+
["component", convertComponentField],
|
|
1251
|
+
["priority", convertPriorityField]
|
|
1252
|
+
]);
|
|
1253
|
+
/**
|
|
1254
|
+
* 转换二元表达式
|
|
1255
|
+
*/
|
|
1256
|
+
function convertBinaryExpression(expr, constantsMap) {
|
|
1257
|
+
var _expr$left, _constantsMap$fieldMa;
|
|
1258
|
+
let field = (_expr$left = expr.left) === null || _expr$left === void 0 ? void 0 : _expr$left.value;
|
|
1259
|
+
const operator = expr.operator;
|
|
1260
|
+
const right = expr.right;
|
|
1261
|
+
if ((field === null || field === void 0 ? void 0 : field.startsWith("\"")) && field.endsWith("\"")) field = field.slice(1, -1);
|
|
1262
|
+
if (!isValidOperator(operator, right)) return "";
|
|
1263
|
+
const handler = SPECIAL_FIELD_HANDLERS.get(field);
|
|
1264
|
+
if (handler) return handler(operator, right, constantsMap);
|
|
1265
|
+
if (field === "labels") return convertTextField("标签", operator, right, constantsMap);
|
|
1266
|
+
if (field === "sprint" || field === "Sprint") return convertSprintField(operator, right, constantsMap);
|
|
1267
|
+
if (field === "text") return convertTextField("text", operator, right, constantsMap);
|
|
1268
|
+
if (field === "created") return convertDateField("created", operator, right, {
|
|
1269
|
+
...constantsMap,
|
|
1270
|
+
fieldMap: {
|
|
1271
|
+
...constantsMap.fieldMap,
|
|
1272
|
+
created: {
|
|
1273
|
+
objectId: "system-created",
|
|
1274
|
+
customFieldKey: "创建时间",
|
|
1275
|
+
fieldTypeKey: "createdAt"
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
if (field === "updated") return convertDateField("updated", operator, right, {
|
|
1280
|
+
...constantsMap,
|
|
1281
|
+
fieldMap: {
|
|
1282
|
+
...constantsMap.fieldMap,
|
|
1283
|
+
updated: {
|
|
1284
|
+
objectId: "system-updated",
|
|
1285
|
+
customFieldKey: "修改时间",
|
|
1286
|
+
fieldTypeKey: "updatedAt"
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
const normalizedFieldName = normalizeFieldName(field);
|
|
1291
|
+
const fieldConfig = (_constantsMap$fieldMa = constantsMap.fieldMap) === null || _constantsMap$fieldMa === void 0 ? void 0 : _constantsMap$fieldMa[normalizedFieldName];
|
|
1292
|
+
if (!fieldConfig || !(fieldConfig.fieldTypeKey in SUPPORTED_FIELD_TYPES)) return "";
|
|
1293
|
+
const fieldType = fieldConfig.fieldTypeKey;
|
|
1294
|
+
if (FIELD_TYPE_GROUPS.user.has(fieldType)) return convertUserField(field, operator, right, constantsMap);
|
|
1295
|
+
if (FIELD_TYPE_GROUPS.version.has(fieldType)) return convertVersionField(field, operator, right, constantsMap);
|
|
1296
|
+
if (FIELD_TYPE_GROUPS.text.has(fieldType)) return convertTextField(field, operator, right, constantsMap);
|
|
1297
|
+
if (FIELD_TYPE_GROUPS.number.has(fieldType)) return convertNumberField(field, operator, right, constantsMap);
|
|
1298
|
+
if (FIELD_TYPE_GROUPS.date.has(fieldType)) return convertDateField(field, operator, right, constantsMap);
|
|
1299
|
+
if (FIELD_TYPE_GROUPS.enum.has(fieldType)) return convertEnumField(field, operator, right, constantsMap);
|
|
1300
|
+
if (fieldType === "UserGroup") return convertUserGroupField(field, operator, right, constantsMap);
|
|
1301
|
+
if (fieldType === "Cascade") return convertCascadeField(field, operator, right, constantsMap);
|
|
1302
|
+
return "";
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* 转换 ORDER BY 子句
|
|
1306
|
+
*/
|
|
1307
|
+
function convertOrderByClause(orderBy, constantsMap) {
|
|
1308
|
+
if (!orderBy || orderBy.type !== AST_NODE_TYPES.ORDER_BY_CLAUSE || !orderBy.fields || orderBy.fields.length === 0) return "";
|
|
1309
|
+
const fieldMap = constantsMap.fieldMap || {};
|
|
1310
|
+
const orderFields = [];
|
|
1311
|
+
for (const orderField of orderBy.fields) {
|
|
1312
|
+
const fieldName = orderField.field;
|
|
1313
|
+
const direction = orderField.direction.toLowerCase();
|
|
1314
|
+
const fieldConfig = fieldMap[normalizeFieldName(fieldName)];
|
|
1315
|
+
const iqlFieldName = (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.customFieldKey) || fieldName;
|
|
1316
|
+
orderFields.push(`${iqlFieldName} ${direction}`);
|
|
1317
|
+
}
|
|
1318
|
+
if (orderFields.length === 0) return "";
|
|
1319
|
+
return `${SQL_CLAUSES.ORDER_BY} ${orderFields.join(",")}`;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* 转换逻辑表达式
|
|
1323
|
+
*/
|
|
1324
|
+
function convertLogicalExpression(expr, constantsMap) {
|
|
1325
|
+
const operator = expr.operator;
|
|
1326
|
+
const left = expr.left ? astToIQL(expr.left, constantsMap) : "";
|
|
1327
|
+
const right = expr.right ? astToIQL(expr.right, constantsMap) : "";
|
|
1328
|
+
if (operator === LOGICAL_OPERATORS.AND) {
|
|
1329
|
+
if (!left || !right) return "";
|
|
1330
|
+
return `${left} ${IQL_LOGICAL_OPERATORS.AND} ${right}`;
|
|
1331
|
+
}
|
|
1332
|
+
if (operator === LOGICAL_OPERATORS.OR) {
|
|
1333
|
+
if (!left || !right) return "";
|
|
1334
|
+
return `${left} ${IQL_LOGICAL_OPERATORS.OR} ${right}`;
|
|
1335
|
+
}
|
|
1336
|
+
if (operator === LOGICAL_OPERATORS.NOT) {
|
|
1337
|
+
if (!left) return "";
|
|
1338
|
+
return `${IQL_LOGICAL_OPERATORS.NOT} ${left}`;
|
|
1339
|
+
}
|
|
1340
|
+
return "";
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
//#endregion
|
|
1344
|
+
//#region src/field-transformer.ts
|
|
1345
|
+
/**
|
|
1346
|
+
* JQL 系统字段到 IQL 字段的映射
|
|
1347
|
+
*/
|
|
1348
|
+
const SYSTEM_FIELD_MAPPING = {
|
|
1349
|
+
project: "workspace",
|
|
1350
|
+
creator: "createdBy",
|
|
1351
|
+
created: "createdAt",
|
|
1352
|
+
updated: "updatedAt",
|
|
1353
|
+
assignee: "assignee",
|
|
1354
|
+
reporter: "reporter",
|
|
1355
|
+
status: "status",
|
|
1356
|
+
Sprint: "sprint",
|
|
1357
|
+
priority: "priority",
|
|
1358
|
+
summary: "name",
|
|
1359
|
+
text: "name",
|
|
1360
|
+
key: "key",
|
|
1361
|
+
"Story Points": "StoryPoint",
|
|
1362
|
+
component: "r_module_management_associated_module",
|
|
1363
|
+
labels: "jira_system_labels",
|
|
1364
|
+
description: "jira_system_description",
|
|
1365
|
+
fixVersion: "version",
|
|
1366
|
+
affectedVersion: "jira_system_versions",
|
|
1367
|
+
environment: "jira_system_environment",
|
|
1368
|
+
resolution: "jira_system_resolution"
|
|
1369
|
+
};
|
|
1370
|
+
/**
|
|
1371
|
+
* FieldType ObjectId 到 FieldTypeKey 的映射
|
|
1372
|
+
*/
|
|
1373
|
+
const FIELD_TYPE_MAPPING = {
|
|
1374
|
+
"NTXX60ceoD": "Name",
|
|
1375
|
+
"na20LzXTA2": "Text",
|
|
1376
|
+
"9SZARgA5TI": "LongText",
|
|
1377
|
+
"4DFeBwRLk9": "LongText",
|
|
1378
|
+
"xCY9ooDKKY": "Number",
|
|
1379
|
+
"CpJDd7io5a": "StoryPoint",
|
|
1380
|
+
"xCv93s1xbd": "Number",
|
|
1381
|
+
"cFdABEFcts": "Date",
|
|
1382
|
+
"euBEAkKOz0": "Date",
|
|
1383
|
+
"xCv93s1xut": "Date",
|
|
1384
|
+
"5nYmdVook2": "Tag",
|
|
1385
|
+
"pgCBxAIJjG": "Dropdown",
|
|
1386
|
+
"3wIfFg5kx4": "Radio",
|
|
1387
|
+
"iW2i992pSp": "Checkbox",
|
|
1388
|
+
"AG5wBe2Ir0": "User",
|
|
1389
|
+
"1g5POUEzw9": "createdBy",
|
|
1390
|
+
"IKeDQI8CrC": "updatedBy",
|
|
1391
|
+
"f59j6P7DhC": "Assignee",
|
|
1392
|
+
"Idh8D5b4sF5": "Reporter",
|
|
1393
|
+
"yCE1bJNxOU": "Version",
|
|
1394
|
+
"809afb6a52": "Version",
|
|
1395
|
+
"an27WsuEID": "UserGroup",
|
|
1396
|
+
"5EXG80C96P": "Cascade",
|
|
1397
|
+
"YE5NQcNDAn": "Priority",
|
|
1398
|
+
"voZko9nkcn": "Status",
|
|
1399
|
+
"rETqSrEpjZ": "Sprint",
|
|
1400
|
+
"EPBm2adxxk": "StatusType",
|
|
1401
|
+
"pN1urEY45D": "Key",
|
|
1402
|
+
"IJ9nXcXYKB": "Workspace",
|
|
1403
|
+
"HBZegUsQdF": "ItemType",
|
|
1404
|
+
"2pvaQp1S5U": "ItemType",
|
|
1405
|
+
"pCHjdxS3Lk": "Tree",
|
|
1406
|
+
"wO2Ei7NTDc": "Tree",
|
|
1407
|
+
"TZwrcPvByG": "File",
|
|
1408
|
+
"DgBnXFOc2P": "Text",
|
|
1409
|
+
"gJxcU0Vy8o": "Number",
|
|
1410
|
+
"pmtLzwTuwd": "Text",
|
|
1411
|
+
"JbI5WXiaFk": "Text",
|
|
1412
|
+
"jGXxAc32dP": "Text"
|
|
1413
|
+
};
|
|
1414
|
+
/**
|
|
1415
|
+
* 创建枚举值映射
|
|
1416
|
+
* @param customData 自定义数据数组
|
|
1417
|
+
* @param labelKey 标签的键名('label' 或 'title')
|
|
1418
|
+
* @param inverse 是否反转映射方向(true: value->label, false: label->value)
|
|
1419
|
+
*/
|
|
1420
|
+
function createValueMap(customData, labelKey = "label", inverse = false) {
|
|
1421
|
+
const map = {};
|
|
1422
|
+
for (const item of customData) {
|
|
1423
|
+
const label = labelKey === "title" ? item.title || item.label : item.label;
|
|
1424
|
+
if (label && item.value) map[inverse ? item.value : label] = inverse ? label : item.value;
|
|
1425
|
+
}
|
|
1426
|
+
return map;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* 将映射添加到多个键
|
|
1430
|
+
*/
|
|
1431
|
+
function addMapToKeys(enumValueMaps, keys, valueMap) {
|
|
1432
|
+
for (const key of keys) enumValueMaps[key] = valueMap;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* 将原始字段数组转换为 ConstantsMap 格式
|
|
1436
|
+
* 这个函数应该在外部调用一次,然后重复使用返回的 ConstantsMap 进行多次转换
|
|
1437
|
+
*
|
|
1438
|
+
* @param fields 原始字段数组
|
|
1439
|
+
* @param projectMap 项目映射(可选)
|
|
1440
|
+
* @param versionMap 版本映射(可选)
|
|
1441
|
+
* @returns ConstantsMap 对象,可直接传给 jql2iql 使用
|
|
1442
|
+
*/
|
|
1443
|
+
function transformFieldsToConstantsMap(fields, projectMap, versionMap) {
|
|
1444
|
+
const fieldMap = {};
|
|
1445
|
+
const enumValueMaps = {};
|
|
1446
|
+
let componentMap;
|
|
1447
|
+
const cascadeMaps = {};
|
|
1448
|
+
for (const field of fields) {
|
|
1449
|
+
const { key, name, objectId, fieldType, data } = field;
|
|
1450
|
+
const fieldTypeKey = FIELD_TYPE_MAPPING[fieldType.objectId] || "Text";
|
|
1451
|
+
const jqlFieldKeys = findJQLFieldKeys(key, name);
|
|
1452
|
+
const fieldInfo = {
|
|
1453
|
+
objectId,
|
|
1454
|
+
customFieldKey: name,
|
|
1455
|
+
fieldTypeKey,
|
|
1456
|
+
fieldTypeId: fieldType.objectId
|
|
1457
|
+
};
|
|
1458
|
+
for (const jqlFieldKey of jqlFieldKeys) fieldMap[jqlFieldKey] = fieldInfo;
|
|
1459
|
+
if (key.startsWith("customfield_")) fieldMap[key.replace("customfield_", "")] = fieldInfo;
|
|
1460
|
+
if ((data === null || data === void 0 ? void 0 : data.customData) && data.customData.length > 0) {
|
|
1461
|
+
const isComponent = key === "r_module_management_associated_module" || name === "所属模块";
|
|
1462
|
+
if (fieldTypeKey === "Cascade") {
|
|
1463
|
+
const valueMap = createValueMap(data.customData, "label", true);
|
|
1464
|
+
addMapToKeys(enumValueMaps, [name, ...jqlFieldKeys], valueMap);
|
|
1465
|
+
Object.assign(cascadeMaps, valueMap);
|
|
1466
|
+
} else if (isComponent) componentMap = createValueMap(data.customData, "title");
|
|
1467
|
+
else {
|
|
1468
|
+
const valueMap = createValueMap(data.customData);
|
|
1469
|
+
if (Object.keys(valueMap).length > 0) addMapToKeys(enumValueMaps, [name, ...jqlFieldKeys], valueMap);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
projectMap,
|
|
1475
|
+
versionMap,
|
|
1476
|
+
component: componentMap,
|
|
1477
|
+
cascade: Object.keys(cascadeMaps).length > 0 ? cascadeMaps : void 0,
|
|
1478
|
+
fieldMap,
|
|
1479
|
+
...enumValueMaps
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* 查找字段的 JQL 字段名(可能返回多个,如"标题"同时映射 text 和 summary)
|
|
1484
|
+
* 对于系统字段,返回 JQL 标准名称;对于自定义字段,返回字段名或 key
|
|
1485
|
+
*/
|
|
1486
|
+
function findJQLFieldKeys(key, name) {
|
|
1487
|
+
const nameToJQLMap = {
|
|
1488
|
+
"标题": ["text", "summary"],
|
|
1489
|
+
"负责人": ["assignee"],
|
|
1490
|
+
"创建人": ["creator"],
|
|
1491
|
+
"创建时间": ["created"],
|
|
1492
|
+
"更新人": ["updatedBy"],
|
|
1493
|
+
"报告人": ["reporter"],
|
|
1494
|
+
"优先级": ["priority"],
|
|
1495
|
+
"状态": ["status"],
|
|
1496
|
+
"迭代": ["Sprint"],
|
|
1497
|
+
"所属模块": ["component"],
|
|
1498
|
+
"标签": ["labels"],
|
|
1499
|
+
"描述": ["description"]
|
|
1500
|
+
};
|
|
1501
|
+
if (nameToJQLMap[name]) return nameToJQLMap[name];
|
|
1502
|
+
const matchedKeys = [];
|
|
1503
|
+
for (const [jqlKey, iqlKey] of Object.entries(SYSTEM_FIELD_MAPPING)) if (key === iqlKey) matchedKeys.push(jqlKey);
|
|
1504
|
+
if (matchedKeys.length > 0) return matchedKeys;
|
|
1505
|
+
return [name];
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
//#endregion
|
|
1509
|
+
//#region src/index.ts
|
|
1510
|
+
/**
|
|
1511
|
+
* 解析 JQL 查询字符串为 AST
|
|
1512
|
+
*/
|
|
1513
|
+
function parseJQL(jqlText) {
|
|
1514
|
+
return buildAST(new JQLParser(new CommonTokenStream(new JQLLexer(CharStreams.fromString(jqlText)))).jqlQuery());
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* 将 JQL 转换为 IQL
|
|
1518
|
+
*/
|
|
1519
|
+
function jql2iql(jqlText, constantsMap) {
|
|
1520
|
+
const ast = parseJQL(jqlText);
|
|
1521
|
+
if (!ast) return "";
|
|
1522
|
+
return astToIQL(ast, constantsMap);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
//#endregion
|
|
1526
|
+
export { astToIQL, buildAST, getNodeInfo, jql2iql, parseJQL, transformFieldsToConstantsMap, traverseJQLTree };
|