mcp-ynab
Version:
Model Context Protocol server for YNAB integration
426 lines (358 loc) • 12.1 kB
JavaScript
/**
* 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 };