UNPKG

mcp-ynab

Version:

Model Context Protocol server for YNAB integration

426 lines (358 loc) 12.1 kB
/** * SubsetServer - Filter cached datasets to serve specific requests * Part of GlobalCacheManager system for Phase 2.5 */ class SubsetServer { constructor() { // Date parsing utilities this.dateRegex = /^\d{4}-\d{2}-\d{2}$/; } /** * Filter transactions based on search criteria */ filterTransactions(transactions, criteria = {}) { let filtered = [...transactions]; // Date filtering if (criteria.sinceDate) { const sinceDate = new Date(criteria.sinceDate); filtered = filtered.filter(t => new Date(t.date) >= sinceDate); } if (criteria.untilDate) { const untilDate = new Date(criteria.untilDate); filtered = filtered.filter(t => new Date(t.date) <= untilDate); } // Account filtering if (criteria.accountId) { filtered = filtered.filter(t => t.account_id === criteria.accountId); } // Category filtering if (criteria.categoryId) { filtered = filtered.filter(t => t.category_id === criteria.categoryId); } // Amount filtering if (criteria.amountMin !== undefined) { filtered = filtered.filter(t => t.amount >= criteria.amountMin); } if (criteria.amountMax !== undefined) { filtered = filtered.filter(t => t.amount <= criteria.amountMax); } // Payee filtering if (criteria.payeeId) { filtered = filtered.filter(t => t.payee_id === criteria.payeeId); } // Text search in payee name if (criteria.payeeSearch) { const searchLower = criteria.payeeSearch.toLowerCase(); filtered = filtered.filter(t => t.payee_name && t.payee_name.toLowerCase().includes(searchLower) ); } // Text search in memo if (criteria.memoSearch) { const searchLower = criteria.memoSearch.toLowerCase(); filtered = filtered.filter(t => t.memo && t.memo.toLowerCase().includes(searchLower) ); } // Flag filtering if (criteria.flagColor) { filtered = filtered.filter(t => t.flag_color === criteria.flagColor); } // Approval status if (criteria.approvedOnly) { filtered = filtered.filter(t => t.approved); } // Cleared status if (criteria.clearedOnly) { filtered = filtered.filter(t => t.cleared === 'cleared'); } // Exclude transfers if (criteria.excludeTransfers) { filtered = filtered.filter(t => !t.transfer_account_id); } // Date range shortcuts if (criteria.dateRange) { const now = new Date(); let startDate; switch (criteria.dateRange) { case 'last_7_days': startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'last_30_days': startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); break; case 'last_3_months': startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate()); break; case 'last_6_months': startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate()); break; case 'last_year': startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); break; case 'year_to_date': startDate = new Date(now.getFullYear(), 0, 1); break; } if (startDate) { filtered = filtered.filter(t => new Date(t.date) >= startDate); } } // Sorting if (criteria.sortBy) { filtered.sort((a, b) => { let aVal, bVal; switch (criteria.sortBy) { case 'date': aVal = new Date(a.date); bVal = new Date(b.date); break; case 'amount': aVal = a.amount; bVal = b.amount; break; case 'payee': aVal = a.payee_name || ''; bVal = b.payee_name || ''; break; default: aVal = a[criteria.sortBy]; bVal = b[criteria.sortBy]; } if (aVal < bVal) return criteria.sortOrder === 'desc' ? 1 : -1; if (aVal > bVal) return criteria.sortOrder === 'desc' ? -1 : 1; return 0; }); } else { // Default sort by date descending (most recent first) filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); } // Limit results if (criteria.limit && criteria.limit > 0) { filtered = filtered.slice(0, criteria.limit); } return filtered; } /** * Filter accounts based on criteria */ filterAccounts(accounts, criteria = {}) { let filtered = [...accounts]; // Include closed accounts filter if (!criteria.includeClosed) { filtered = filtered.filter(a => !a.closed); } // Account type filtering if (criteria.accountType) { filtered = filtered.filter(a => a.type === criteria.accountType); } // On-budget vs off-budget if (criteria.onBudget !== undefined) { filtered = filtered.filter(a => a.on_budget === criteria.onBudget); } // Balance filtering if (criteria.minBalance !== undefined) { filtered = filtered.filter(a => a.balance >= criteria.minBalance); } if (criteria.maxBalance !== undefined) { filtered = filtered.filter(a => a.balance <= criteria.maxBalance); } // Name search if (criteria.nameSearch) { const searchLower = criteria.nameSearch.toLowerCase(); filtered = filtered.filter(a => a.name.toLowerCase().includes(searchLower) ); } return filtered; } /** * Filter categories based on criteria */ filterCategories(categoryGroups, criteria = {}) { let filtered = JSON.parse(JSON.stringify(categoryGroups)); // Deep clone // Filter by category group if (criteria.groupId) { filtered = filtered.filter(group => group.id === criteria.groupId); } // Filter categories within groups filtered.forEach(group => { if (group.categories) { let categories = group.categories; // Hidden categories filter if (!criteria.includeHidden) { categories = categories.filter(c => !c.hidden); } // Balance filtering if (criteria.minBalance !== undefined) { categories = categories.filter(c => c.balance >= criteria.minBalance); } if (criteria.maxBalance !== undefined) { categories = categories.filter(c => c.balance <= criteria.maxBalance); } // Budgeted amount filtering if (criteria.minBudgeted !== undefined) { categories = categories.filter(c => c.budgeted >= criteria.minBudgeted); } // Activity filtering if (criteria.hasActivity !== undefined) { if (criteria.hasActivity) { categories = categories.filter(c => c.activity !== 0); } else { categories = categories.filter(c => c.activity === 0); } } // Goal filtering if (criteria.hasGoal !== undefined) { if (criteria.hasGoal) { categories = categories.filter(c => c.goal_type); } else { categories = categories.filter(c => !c.goal_type); } } // Name search if (criteria.nameSearch) { const searchLower = criteria.nameSearch.toLowerCase(); categories = categories.filter(c => c.name.toLowerCase().includes(searchLower) ); } group.categories = categories; } }); // Remove empty groups if no categories match if (criteria.removeEmptyGroups) { filtered = filtered.filter(group => group.categories && group.categories.length > 0 ); } return filtered; } /** * Filter payees based on criteria */ filterPayees(payees, criteria = {}) { let filtered = [...payees]; // Name search if (criteria.nameSearch) { const searchLower = criteria.nameSearch.toLowerCase(); filtered = filtered.filter(p => p.name.toLowerCase().includes(searchLower) ); } // Transaction count filtering (if payee has transaction stats) if (criteria.minTransactionCount !== undefined) { filtered = filtered.filter(p => p.transaction_count >= criteria.minTransactionCount ); } // Include/exclude transfer payees if (criteria.excludeTransferPayees) { filtered = filtered.filter(p => !p.transfer_account_id); } // Sorting if (criteria.sortBy) { filtered.sort((a, b) => { let aVal, bVal; switch (criteria.sortBy) { case 'name': aVal = a.name.toLowerCase(); bVal = b.name.toLowerCase(); break; case 'transaction_count': aVal = a.transaction_count || 0; bVal = b.transaction_count || 0; break; case 'total_spent': aVal = a.total_spent || 0; bVal = b.total_spent || 0; break; default: aVal = a[criteria.sortBy]; bVal = b[criteria.sortBy]; } if (aVal < bVal) return criteria.sortOrder === 'desc' ? 1 : -1; if (aVal > bVal) return criteria.sortOrder === 'desc' ? -1 : 1; return 0; }); } return filtered; } /** * Filter scheduled transactions */ filterScheduledTransactions(scheduledTransactions, criteria = {}) { let filtered = [...scheduledTransactions]; // Account filtering if (criteria.accountId) { filtered = filtered.filter(st => st.account_id === criteria.accountId); } // Category filtering if (criteria.categoryId) { filtered = filtered.filter(st => st.category_id === criteria.categoryId); } // Frequency filtering if (criteria.frequency) { filtered = filtered.filter(st => st.frequency === criteria.frequency); } // Amount filtering if (criteria.minAmount !== undefined) { filtered = filtered.filter(st => st.amount >= criteria.minAmount); } if (criteria.maxAmount !== undefined) { filtered = filtered.filter(st => st.amount <= criteria.maxAmount); } // Upcoming filtering (next occurrence within specified days) if (criteria.upcomingDays) { const now = new Date(); const cutoffDate = new Date(now.getTime() + criteria.upcomingDays * 24 * 60 * 60 * 1000); filtered = filtered.filter(st => { const nextDate = new Date(st.date_first); return nextDate <= cutoffDate; }); } return filtered; } /** * Apply multiple filters to a dataset */ applyFilters(data, dataType, criteria) { switch (dataType) { case 'transactions': return this.filterTransactions(data, criteria); case 'accounts': return this.filterAccounts(data, criteria); case 'categories': return this.filterCategories(data, criteria); case 'payees': return this.filterPayees(data, criteria); case 'scheduled_transactions': return this.filterScheduledTransactions(data, criteria); default: console.warn(`[SubsetServer] Unknown data type: ${dataType}`); return data; } } /** * Get summary statistics for filtered data */ getFilteredStats(originalData, filteredData, dataType) { const stats = { originalCount: Array.isArray(originalData) ? originalData.length : 0, filteredCount: Array.isArray(filteredData) ? filteredData.length : 0, filterEfficiency: 0, dataType }; if (stats.originalCount > 0) { stats.filterEfficiency = ((stats.originalCount - stats.filteredCount) / stats.originalCount) * 100; } // Add type-specific stats if (dataType === 'transactions' && Array.isArray(filteredData)) { const totalAmount = filteredData.reduce((sum, t) => sum + t.amount, 0); stats.totalAmount = totalAmount; stats.averageAmount = filteredData.length > 0 ? totalAmount / filteredData.length : 0; } return stats; } } export { SubsetServer };