@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
308 lines • 15.7 kB
JavaScript
/**
* 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