UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

308 lines 15.7 kB
/** * Project filtering utility for Optimizely MCP Server * @description Handles filtering and validation of projects based on configuration */ import { getLogger } from '../logging/Logger.js'; // CRITICAL: SafeIdConverter prevents scientific notation in large IDs // DO NOT REMOVE OR REPLACE WITH toString() - This caused duplicate records bug // Evidence: 5.02479302872269e+15 vs 5024793028722690 for same entity // Fixed: January 26, 2025 - See CONTEXT-RECOVERY-V2.md for details import { safeIdToString } from '../utils/SafeIdConverter.js'; /** * Project filtering service that applies configuration-based project selection * @description Ensures only authorized projects are synced based on user configuration */ export class ProjectFilter { projectConfig; /** * Creates a new ProjectFilter instance * @param projectConfig - Project configuration from the main config */ constructor(projectConfig) { this.projectConfig = projectConfig; } /** * Filters a list of projects based on the configuration * @param allProjects - Array of all projects from the API * @returns Promise resolving to filtered array of projects * @throws {Error} When filtering results in no projects or exceeds limits */ async filterProjects(allProjects) { if (!allProjects || allProjects.length === 0) { throw new Error('No projects available from Optimizely API'); } getLogger().info({ availableCount: allProjects.length }, 'ProjectFilter: Processing available projects'); let filteredProjects = []; // Check if any filters are configured const hasIdFilter = this.projectConfig.allowedIds && this.projectConfig.allowedIds.length > 0; const hasNameFilter = this.projectConfig.allowedNames && this.projectConfig.allowedNames.length > 0; const hasNoFilters = !hasIdFilter && !hasNameFilter; // CRITICAL DEBUGGING: Log current filter state before applying getLogger().error({ method: 'filterProjects', currentAllowedIds: this.projectConfig.allowedIds ? [...this.projectConfig.allowedIds] : null, allowedIdsCount: this.projectConfig.allowedIds?.length || 0, autoDiscoverAll: this.projectConfig.autoDiscoverAll, hasIdFilter, hasNameFilter, hasNoFilters, totalProjectsToFilter: allProjects.length, operation: 'FILTER_START' }, '🔥 CRITICAL DEBUG: Starting project filtering with current in-memory state'); // Apply filtering based on configuration if (this.projectConfig.autoDiscoverAll) { getLogger().info('ProjectFilter: Auto-discover mode enabled - including all accessible projects'); filteredProjects = [...allProjects]; } else if (hasNoFilters) { getLogger().info('ProjectFilter: No filters configured - including all accessible projects'); filteredProjects = [...allProjects]; } else { // Filter by IDs first (highest priority) if (hasIdFilter) { // CRITICAL: Use SafeIdConverter for project IDs to prevent scientific notation const allowedIds = new Set(this.projectConfig.allowedIds.map(id => safeIdToString(id))); // CRITICAL DEBUGGING: Show exactly what IDs we're filtering by getLogger().error({ method: 'filterProjects', allowedIds: Array.from(allowedIds), allowedIdsFromMemory: this.projectConfig.allowedIds ? [...this.projectConfig.allowedIds] : [], allProjectIds: allProjects.map(p => ({ id: p.id, name: p.name })), operation: 'APPLYING_ID_FILTER' }, '🔥 CRITICAL DEBUG: Applying ID filter with in-memory allowed IDs'); const matchedByIds = allProjects.filter(project => { const projectId = project.id ? safeIdToString(project.id) : ''; const projectIdAlt = project.project_id ? safeIdToString(project.project_id) : ''; const isMatch = allowedIds.has(projectId) || allowedIds.has(projectIdAlt); if (!isMatch) { getLogger().debug({ projectName: project.name, projectId, projectIdAlt, allowedIds: Array.from(allowedIds) }, 'ProjectFilter: Project excluded by ID filter'); } return isMatch; }); filteredProjects.push(...matchedByIds); getLogger().info({ matchedCount: matchedByIds.length, allowedIds: this.projectConfig.allowedIds }, 'ProjectFilter: Found projects matching IDs'); } // Filter by name patterns (additive to IDs) if (hasNameFilter) { const matchedByNames = allProjects.filter(project => { const projectName = project.name || ''; return this.projectConfig.allowedNames.some(pattern => this.matchesPattern(projectName, pattern)); }); // Add projects not already included by ID filtering const existingIds = new Set(filteredProjects.map(p => p.id || p.project_id)); const newMatches = matchedByNames.filter(project => !existingIds.has(project.id || project.project_id)); filteredProjects.push(...newMatches); getLogger().info({ additionalCount: newMatches.length, allowedNames: this.projectConfig.allowedNames }, 'ProjectFilter: Found additional projects matching names'); } } // Remove duplicates based on project ID const uniqueProjects = this.removeDuplicates(filteredProjects); getLogger().debug({ uniqueCount: uniqueProjects.length }, 'ProjectFilter: Unique projects after deduplication'); // Apply safety limits only if explicitly configured if (this.projectConfig.maxProjects && this.projectConfig.maxProjects > 0) { const maxProjects = this.projectConfig.maxProjects; if (uniqueProjects.length > maxProjects) { getLogger().warn({ foundCount: uniqueProjects.length, maxLimit: maxProjects }, 'ProjectFilter: Project count exceeds configured limit, truncating to max allowed'); uniqueProjects.splice(maxProjects); } } // Final validation if (uniqueProjects.length === 0) { const configInfo = this.getConfigurationSummary(); throw new Error(`No projects matched the configured filters. ${configInfo}`); } // CRITICAL DEBUGGING: Log final filtering results getLogger().error({ method: 'filterProjects', finalSelectedProjects: uniqueProjects.map(p => ({ id: p.id || p.project_id, name: p.name })), selectedCount: uniqueProjects.length, originalCount: allProjects.length, filterUsed: hasIdFilter ? 'ID_FILTER' : hasNameFilter ? 'NAME_FILTER' : 'NO_FILTER', memoryAllowedIds: this.projectConfig.allowedIds ? [...this.projectConfig.allowedIds] : [], operation: 'FILTER_COMPLETE' }, '🔥 CRITICAL DEBUG: Project filtering complete - these projects will have entities synced'); getLogger().info({ selectedCount: uniqueProjects.length }, 'ProjectFilter: Final project selection completed'); uniqueProjects.forEach(project => { getLogger().debug({ projectName: project.name, projectId: project.id || project.project_id }, 'ProjectFilter: Selected project'); }); return uniqueProjects; } /** * Checks if a project name matches a pattern (supports basic wildcards) * @param projectName - The project name to test * @param pattern - The pattern to match against (supports * wildcard) * @returns True if the name matches the pattern * @private */ matchesPattern(projectName, pattern) { if (pattern === projectName) { return true; // Exact match } // Simple wildcard support (* means any characters) if (pattern.includes('*')) { const regexPattern = pattern .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special regex chars .replace(/\\*/g, '.*'); // Convert * to .* const regex = new RegExp(`^${regexPattern}$`, 'i'); // Case insensitive return regex.test(projectName); } // Case-insensitive partial match if no wildcards return projectName.toLowerCase().includes(pattern.toLowerCase()); } /** * Removes duplicate projects based on ID * @param projects - Array of projects that may contain duplicates * @returns Array of unique projects * @private */ removeDuplicates(projects) { const seen = new Set(); return projects.filter(project => { const id = project.id || project.project_id; if (seen.has(id)) { return false; } seen.add(id); return true; }); } /** * Gets a summary of the current configuration for error messages * @returns String describing the current filter configuration * @private */ getConfigurationSummary() { const parts = []; if (this.projectConfig.allowedIds && this.projectConfig.allowedIds.length > 0) { parts.push(`Project IDs: [${this.projectConfig.allowedIds.join(', ')}]`); } if (this.projectConfig.allowedNames && this.projectConfig.allowedNames.length > 0) { parts.push(`Name patterns: [${this.projectConfig.allowedNames.join(', ')}]`); } if (this.projectConfig.autoDiscoverAll) { parts.push('Auto-discover: enabled'); } if (this.projectConfig.maxProjects && this.projectConfig.maxProjects > 0) { parts.push(`Max projects: ${this.projectConfig.maxProjects}`); } else { parts.push('Max projects: unlimited'); } return `Current filter configuration - ${parts.join(', ')}`; } /** * Validates the project configuration before use * @throws {Error} When configuration is invalid */ validateConfiguration() { const hasIds = this.projectConfig.allowedIds && this.projectConfig.allowedIds.length > 0; const hasNames = this.projectConfig.allowedNames && this.projectConfig.allowedNames.length > 0; const autoDiscover = this.projectConfig.autoDiscoverAll; if (!hasIds && !hasNames && !autoDiscover) { throw new Error('Project filter configuration is invalid: must specify allowedIds, allowedNames, or enable autoDiscoverAll'); } // Validate IDs are not empty strings if (hasIds) { const invalidIds = this.projectConfig.allowedIds.filter(id => !id || id.trim() === ''); if (invalidIds.length > 0) { throw new Error('Project filter configuration contains empty or invalid project IDs'); } } // Validate name patterns are not empty if (hasNames) { const invalidNames = this.projectConfig.allowedNames.filter(name => !name || name.trim() === ''); if (invalidNames.length > 0) { throw new Error('Project filter configuration contains empty or invalid project name patterns'); } } getLogger().debug('ProjectFilter: Configuration validation passed'); } /** * Gets the maximum number of projects that should be processed * @returns The configured maximum number of projects, or 0 for unlimited */ getMaxProjects() { return this.projectConfig.maxProjects || 0; } /** * Checks if auto-discover mode is enabled * @returns True if auto-discover is enabled */ isAutoDiscoverEnabled() { return Boolean(this.projectConfig.autoDiscoverAll); } /** * DIAGNOSTIC: Gets current in-memory filter state for debugging * @returns Current filter configuration and state */ getFilterDiagnostics() { const hasIds = this.projectConfig.allowedIds && this.projectConfig.allowedIds.length > 0; const hasNames = this.projectConfig.allowedNames && this.projectConfig.allowedNames.length > 0; return { allowedIds: this.projectConfig.allowedIds ? [...this.projectConfig.allowedIds] : [], allowedNames: this.projectConfig.allowedNames ? [...this.projectConfig.allowedNames] : [], autoDiscoverAll: Boolean(this.projectConfig.autoDiscoverAll), maxProjects: this.projectConfig.maxProjects || 0, hasFilters: hasIds || hasNames || Boolean(this.projectConfig.autoDiscoverAll), instanceInfo: `ProjectFilter_Instance_${Math.random().toString(36).substr(2, 9)}` }; } /** * Gets the allowed project IDs for filtering * @returns Array of allowed project IDs, or empty array if none configured */ getAllowedProjectIds() { if (!this.projectConfig.allowedIds || this.projectConfig.allowedIds.length === 0) { return []; } // CRITICAL: Use SafeIdConverter to ensure consistent ID format return this.projectConfig.allowedIds.map(id => safeIdToString(id)); } /** * Dynamically adds a new project ID to the allowed list * @param projectId - The project ID to add to the allowed list * @description This method allows runtime addition of project IDs to the filter, * which is useful for automatically including locally-created projects * regardless of the initial OPTIMIZELY_PROJECT_IDS configuration. */ addAllowedProjectId(projectId) { // Initialize allowedIds array if it doesn't exist if (!this.projectConfig.allowedIds) { this.projectConfig.allowedIds = []; } const safeProjectId = safeIdToString(projectId); // CRITICAL DEBUGGING: Log the current state before modification getLogger().error({ method: 'addAllowedProjectId', newProjectId: safeProjectId, currentAllowedIds: this.projectConfig.allowedIds ? [...this.projectConfig.allowedIds] : [], currentCount: this.projectConfig.allowedIds?.length || 0, instanceId: `ProjectFilter_${Math.random().toString(36).substr(2, 9)}` }, '🔥 CRITICAL DEBUG: Adding project to in-memory filter'); // Check if already exists to avoid duplicates if (!this.projectConfig.allowedIds.includes(safeProjectId)) { this.projectConfig.allowedIds.push(safeProjectId); // CRITICAL DEBUGGING: Log the state after modification getLogger().error({ method: 'addAllowedProjectId', projectId: safeProjectId, updatedAllowedIds: [...this.projectConfig.allowedIds], totalAllowedProjects: this.projectConfig.allowedIds.length, operation: 'ADDED_TO_MEMORY' }, '🔥 CRITICAL DEBUG: Successfully added project to in-memory filter - THIS PROJECT SHOULD NOW BE INCLUDED IN FUTURE SYNCS'); } else { getLogger().debug({ projectId: safeProjectId, existingAllowedIds: [...this.projectConfig.allowedIds] }, 'ProjectFilter: Project ID already in allowed list'); } } } //# sourceMappingURL=ProjectFilter.js.map