Skip to main content
Glama
UniversalSearchService.ts8.86 kB
/** * UniversalSearchService - Centralized record search operations * * Issue #574: Refactored to use Strategy Pattern for resource-specific search logic * Issue #935: Search routing delegated to SearchCoordinator and extracted services */ import { UniversalResourceType, SearchType, MatchType, SortType, } from '../handlers/tool-configs/universal/types.js'; import type { UniversalSearchParams } from '../handlers/tool-configs/universal/types.js'; import { AttioRecord } from '../types/attio.js'; import { performance } from 'perf_hooks'; import { debug } from '../utils/logger.js'; // Import services import { ValidationService } from './ValidationService.js'; import { CachingService } from './CachingService.js'; // Import performance tracking import { enhancedPerformanceTracker } from '../middleware/performance-enhanced.js'; // Import timeframe utility functions for Issue #475 import { convertDateParamsToTimeframeQuery } from '../utils/filters/timeframe-utils.js'; // Issue #935: Search routing delegated to SearchCoordinator import { SearchCoordinator } from './search/SearchCoordinator.js'; /** * UniversalSearchService provides centralized record search functionality * Issue #935: Strategy initialization delegated to StrategyFactory via SearchCoordinator */ export class UniversalSearchService { /** * Universal search handler with performance tracking */ static async searchRecords( params: UniversalSearchParams ): Promise<AttioRecord[]> { const { resource_type, query, filters, limit, offset, search_type = SearchType.BASIC, fields, match_type = MatchType.PARTIAL, sort = SortType.NAME, // New TC search parameters relationship_target_type, relationship_target_id, timeframe_attribute, start_date, end_date, date_operator, content_fields, use_or_logic, // Issue #475: New date filtering parameters date_from, date_to, created_after, created_before, updated_after, updated_before, timeframe, date_field, } = params; // Start performance tracking const perfId = enhancedPerformanceTracker.startOperation( 'search-records', 'search', { resourceType: resource_type, hasQuery: !!query, hasFilters: !!(filters && Object.keys(filters).length > 0), limit, offset, searchType: search_type, hasFields: !!(fields && fields.length > 0), matchType: match_type, sortType: sort, } ); // Track validation timing const validationStart = performance.now(); // Validate pagination parameters using ValidationService ValidationService.validatePaginationParameters({ limit, offset }, perfId); // Validate filter schema for malformed advanced filters ValidationService.validateFiltersSchema(filters); enhancedPerformanceTracker.markTiming( perfId, 'validation', performance.now() - validationStart ); // Issue #475: Convert user-friendly date parameters to API format let processedTimeframeParams = { timeframe_attribute, start_date, end_date, date_operator, }; try { const dateConversion = convertDateParamsToTimeframeQuery({ date_from, date_to, created_after, created_before, updated_after, updated_before, timeframe, date_field, }); if (dateConversion) { // Use converted parameters, prioritizing user-friendly parameters processedTimeframeParams = { ...processedTimeframeParams, ...dateConversion, }; } } catch (dateError: unknown) { // Re-throw date validation errors with helpful context const errorMessage = dateError instanceof Error ? `Date parameter validation failed: ${dateError.message}` : 'Invalid date parameters provided'; throw new Error(errorMessage); } // Auto-detect timeframe searches and FORCE them to use the Query API let finalSearchType = search_type; const hasTimeframeParams = processedTimeframeParams.timeframe_attribute && (processedTimeframeParams.start_date || processedTimeframeParams.end_date); if (hasTimeframeParams) { finalSearchType = SearchType.TIMEFRAME; debug( 'UniversalSearchService', 'FORCING timeframe search to use Query API (advanced search API does not support date comparisons)', { originalSearchType: search_type, timeframe_attribute: processedTimeframeParams.timeframe_attribute, start_date: processedTimeframeParams.start_date, end_date: processedTimeframeParams.end_date, date_operator: processedTimeframeParams.date_operator, } ); } // Track API call timing const apiStart = enhancedPerformanceTracker.markApiStart(perfId); let results: AttioRecord[]; try { results = await this.performSearchByResourceType(resource_type, { query, filters, limit, offset, search_type: finalSearchType, fields, match_type, sort, // New TC search parameters relationship_target_type, relationship_target_id, // Use processed timeframe parameters (Issue #475) timeframe_attribute: processedTimeframeParams.timeframe_attribute, start_date: processedTimeframeParams.start_date, end_date: processedTimeframeParams.end_date, date_operator: processedTimeframeParams.date_operator, content_fields, use_or_logic, }); enhancedPerformanceTracker.markApiEnd(perfId, apiStart); enhancedPerformanceTracker.endOperation(perfId, true, undefined, 200, { recordCount: results.length, }); return results; } catch (apiError: unknown) { enhancedPerformanceTracker.markApiEnd(perfId, apiStart); const errorObj = apiError as Record<string, unknown>; const statusCode = ((errorObj?.response as Record<string, unknown>)?.status as number) || (errorObj?.statusCode as number) || 500; const errorMessage = apiError instanceof Error ? apiError.message : 'Search failed'; enhancedPerformanceTracker.endOperation( perfId, false, errorMessage, statusCode ); throw apiError; } } /** * Perform search by resource type * Issue #935: Delegates to SearchCoordinator for routing logic */ private static async performSearchByResourceType( resource_type: UniversalResourceType, params: { query?: string; filters?: Record<string, unknown>; limit?: number; offset?: number; search_type?: SearchType; fields?: string[]; match_type?: MatchType; sort?: SortType; relationship_target_type?: UniversalResourceType; relationship_target_id?: string; timeframe_attribute?: string; start_date?: string; end_date?: string; date_operator?: 'greater_than' | 'less_than' | 'between' | 'equals'; content_fields?: string[]; use_or_logic?: boolean; } ): Promise<AttioRecord[]> { // Issue #935: Delegate to SearchCoordinator for all search routing return SearchCoordinator.executeSearch({ resource_type, ...params, }); } // Utility methods static async getSearchSuggestions(): Promise<string[]> { return []; } static async getRecordCount( resource_type: UniversalResourceType ): Promise<number> { switch (resource_type) { case UniversalResourceType.TASKS: { const cachedTasks = CachingService.getCachedTasks('tasks_cache'); return cachedTasks ? cachedTasks.length : -1; } default: return -1; } } static supportsAdvancedFiltering( resource_type: UniversalResourceType ): boolean { switch (resource_type) { case UniversalResourceType.COMPANIES: case UniversalResourceType.PEOPLE: return true; case UniversalResourceType.LISTS: case UniversalResourceType.RECORDS: case UniversalResourceType.DEALS: case UniversalResourceType.TASKS: return false; default: return false; } } static supportsQuerySearch(resource_type: UniversalResourceType): boolean { switch (resource_type) { case UniversalResourceType.COMPANIES: case UniversalResourceType.PEOPLE: case UniversalResourceType.LISTS: return true; case UniversalResourceType.RECORDS: case UniversalResourceType.DEALS: case UniversalResourceType.TASKS: return false; default: return false; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server