UNPKG

taiga-mcp-server

Version:

Full-featured MCP server for Taiga project management with advanced sprint and issue tracking. Based on mcpTAIGA by adriapedralbes, enhanced with substantial modifications. Developed collaboratively with Claude Code (AI-assisted development).

369 lines (320 loc) 9.29 kB
/** * 高級查詢語法解析器 * Advanced Query Parser for Taiga MCP Server */ import { OPERATORS, LOGIC_OPERATORS, SORT_DIRECTIONS, TIME_KEYWORDS, VALIDATION_RULES, FIELD_TYPES } from './queryGrammar.js'; export class QueryParser { constructor() { this.tokens = []; this.position = 0; } /** * 解析查詢字符串 * @param {string} queryString - 查詢字符串 * @param {string} type - 數據類型 (ISSUE, USER_STORY, TASK) * @returns {Object} 解析後的查詢對象 */ parse(queryString, type = 'ISSUE') { if (!queryString || typeof queryString !== 'string') { throw new Error('查詢字符串不能為空'); } this.tokens = this.tokenize(queryString); this.position = 0; this.dataType = type; const query = { filters: [], logic: 'AND', orderBy: null, limit: null, groupBy: null, type: type }; try { this.parseExpression(query); return query; } catch (error) { throw new Error(`查詢解析錯誤: ${error.message}`); } } /** * 分詞器 - 將查詢字符串分解為標記 */ tokenize(queryString) { const tokens = []; const regex = /([A-Za-z_][A-Za-z0-9_]*):([><=!~]*)([^:\s()]+|\([^)]+\)|"[^"]*")|(\bAND\b|\bOR\b|\bNOT\b|\bORDER\s+BY\b|\bLIMIT\b|\bGROUP\s+BY\b)|([()])|(\S+)/gi; let match; while ((match = regex.exec(queryString)) !== null) { if (match[1] && match[3]) { // 字段查詢: field:operator:value tokens.push({ type: 'FIELD_QUERY', field: match[1].toLowerCase(), operator: match[2] || '=', value: this.parseValue(match[3]) }); } else if (match[4]) { // 邏輯操作符或關鍵字 const keyword = match[4].toUpperCase().replace(/\s+/g, '_'); if (keyword === 'ORDER_BY') { tokens.push({ type: 'ORDER_BY' }); } else if (keyword === 'GROUP_BY') { tokens.push({ type: 'GROUP_BY' }); } else if (keyword === 'LIMIT') { tokens.push({ type: 'LIMIT' }); } else { tokens.push({ type: 'LOGIC', operator: keyword }); } } else if (match[5]) { // 括號 tokens.push({ type: 'PAREN', value: match[5] }); } else if (match[6]) { // 其他標記 tokens.push({ type: 'VALUE', value: match[6] }); } } return tokens; } /** * 解析值並處理特殊格式 */ parseValue(valueString) { // 移除引號 if ((valueString.startsWith('"') && valueString.endsWith('"')) || (valueString.startsWith("'") && valueString.endsWith("'"))) { return valueString.slice(1, -1); } // 處理括號內的數組 [item1,item2,item3] if (valueString.startsWith('(') && valueString.endsWith(')')) { const arrayContent = valueString.slice(1, -1); return arrayContent.split(',').map(item => item.trim()); } // 處理範圍查詢 3..8 if (valueString.includes('..')) { const [start, end] = valueString.split('..'); return { range: [this.parseNumericValue(start), this.parseNumericValue(end)] }; } // 處理數值 if (!isNaN(valueString)) { return this.parseNumericValue(valueString); } // 處理時間關鍵字 if (TIME_KEYWORDS[valueString]) { return TIME_KEYWORDS[valueString](); } // 處理相對時間 <7d, >30d const timeMatch = valueString.match(/^([<>]=?)(\d+)([dwmy])$/); if (timeMatch) { const operator = timeMatch[1]; const value = parseInt(timeMatch[2]); const unit = timeMatch[3]; let milliseconds; switch (unit) { case 'd': milliseconds = value * 24 * 60 * 60 * 1000; break; case 'w': milliseconds = value * 7 * 24 * 60 * 60 * 1000; break; case 'm': milliseconds = value * 30 * 24 * 60 * 60 * 1000; break; case 'y': milliseconds = value * 365 * 24 * 60 * 60 * 1000; break; } const targetDate = new Date(Date.now() - milliseconds); return { relativeTime: operator, date: targetDate }; } return valueString; } /** * 解析數值 */ parseNumericValue(value) { const num = Number(value); return isNaN(num) ? value : num; } /** * 解析表達式 */ parseExpression(query) { while (this.position < this.tokens.length) { const token = this.tokens[this.position]; switch (token.type) { case 'FIELD_QUERY': this.parseFieldQuery(query, token); break; case 'LOGIC': this.parseLogicOperator(query, token); break; case 'ORDER_BY': this.parseOrderBy(query); break; case 'LIMIT': this.parseLimit(query); break; case 'GROUP_BY': this.parseGroupBy(query); break; case 'PAREN': this.parseParentheses(query, token); break; default: this.position++; break; } } } /** * 解析字段查詢 */ parseFieldQuery(query, token) { const { field, operator, value } = token; // 驗證字段 if (!VALIDATION_RULES.isValidField(field, this.dataType)) { throw new Error(`無效的字段: ${field}`); } // 標準化操作符 let normalizedOperator = this.normalizeOperator(operator); // 驗證操作符 if (!VALIDATION_RULES.isValidOperator(normalizedOperator)) { throw new Error(`無效的操作符: ${operator}`); } // 驗證值 if (!VALIDATION_RULES.isValidValue(field, value, this.dataType)) { console.warn(`字段 ${field} 的值可能無效: ${value}`); } query.filters.push({ field, operator: normalizedOperator, value, originalOperator: operator }); this.position++; } /** * 標準化操作符 */ normalizeOperator(operator) { switch (operator) { case '': case '=': return OPERATORS.EQUAL; case '!=': return OPERATORS.NOT_EQUAL; case '>': return OPERATORS.GREATER_THAN; case '>=': return OPERATORS.GREATER_EQUAL; case '<': return OPERATORS.LESS_THAN; case '<=': return OPERATORS.LESS_EQUAL; case '~': return OPERATORS.FUZZY; default: return operator; } } /** * 解析邏輯操作符 */ parseLogicOperator(query, token) { if (token.operator === 'AND' || token.operator === 'OR') { query.logic = token.operator; } this.position++; } /** * 解析排序子句 */ parseOrderBy(query) { this.position++; // 跳過 ORDER BY if (this.position < this.tokens.length) { const fieldToken = this.tokens[this.position]; const field = fieldToken.value || fieldToken.field; let direction = SORT_DIRECTIONS.ASC; if (this.position + 1 < this.tokens.length) { const directionToken = this.tokens[this.position + 1]; if (directionToken.value && (directionToken.value.toUpperCase() === 'DESC' || directionToken.value.toUpperCase() === 'ASC')) { direction = directionToken.value.toUpperCase(); this.position++; } } query.orderBy = { field, direction }; this.position++; } } /** * 解析限制子句 */ parseLimit(query) { this.position++; // 跳過 LIMIT if (this.position < this.tokens.length) { const limitToken = this.tokens[this.position]; const limit = parseInt(limitToken.value); if (!isNaN(limit) && limit > 0) { query.limit = limit; } this.position++; } } /** * 解析分組子句 */ parseGroupBy(query) { this.position++; // 跳過 GROUP BY if (this.position < this.tokens.length) { const fieldToken = this.tokens[this.position]; query.groupBy = fieldToken.value || fieldToken.field; this.position++; } } /** * 解析括號(用於邏輯分組) */ parseParentheses(query, token) { // 簡化處理,暫時跳過括號 // 完整實現需要處理嵌套邏輯表達式 this.position++; } /** * 驗證查詢對象 */ validateQuery(query) { if (!query.filters || query.filters.length === 0) { throw new Error('查詢必須至少包含一個過濾條件'); } // 驗證每個過濾條件 for (const filter of query.filters) { if (!filter.field || !filter.operator) { throw new Error('過濾條件必須包含字段和操作符'); } } return true; } /** * 獲取查詢統計信息 */ getQueryStats(query) { return { filterCount: query.filters.length, hasOrderBy: !!query.orderBy, hasLimit: !!query.limit, hasGroupBy: !!query.groupBy, complexity: this.calculateComplexity(query) }; } /** * 計算查詢複雜度 */ calculateComplexity(query) { let complexity = query.filters.length; if (query.orderBy) complexity += 1; if (query.limit) complexity += 0.5; if (query.groupBy) complexity += 2; return Math.round(complexity * 10) / 10; } } export default QueryParser;