Skip to main content
Glama
BaseSearchStrategy.ts4.2 kB
/** * Base search strategy class with common functionality * Issue #574: Extract resource-specific search strategies */ import { AttioRecord } from '../../types/attio.js'; import { warn } from '../../utils/logger.js'; import { MatchType, SortType, } from '../../handlers/tool-configs/universal/types.js'; import { ISearchStrategy, SearchStrategyParams, StrategyDependencies, TimeframeParams, } from './interfaces.js'; /** * Abstract base class for search strategies */ export abstract class BaseSearchStrategy implements ISearchStrategy { protected dependencies: StrategyDependencies; constructor(dependencies: StrategyDependencies) { this.dependencies = dependencies; } abstract search(params: SearchStrategyParams): Promise<AttioRecord[]>; abstract getResourceType(): string; abstract supportsAdvancedFiltering(): boolean; abstract supportsQuerySearch(): boolean; /** * Apply timeframe filtering by merging with existing filters */ protected applyTimeframeFiltering( filters: Record<string, unknown> | undefined, timeframeParams?: TimeframeParams ): Record<string, unknown> | undefined { if ( !timeframeParams?.timeframe_attribute || (!timeframeParams.start_date && !timeframeParams.end_date) ) { return filters; } if ( !this.dependencies.createDateFilter || !this.dependencies.mergeFilters ) { warn('BaseSearchStrategy', 'Date filtering dependencies not available'); return filters; } const dateFilter = this.dependencies.createDateFilter(timeframeParams); if (dateFilter) { return this.dependencies.mergeFilters(filters, dateFilter); } return filters; } /** * Apply relevance ranking to results */ protected applyRelevanceRanking( results: AttioRecord[], query: string, searchFields: string[], sort: SortType = SortType.NAME ): AttioRecord[] { if (sort === SortType.RELEVANCE && this.dependencies.rankByRelevance) { return this.dependencies.rankByRelevance(results, query, searchFields); } return results; } /** * Check if a search looks like a domain */ protected looksLikeDomain(query: string): boolean { return ( query.includes('.') || query.includes('www') || query.includes('http') || /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(query) ); } /** * Check if a search looks like an email */ protected looksLikeEmail(query: string): boolean { return query.includes('@') && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query); } /** * Create content search filters */ protected createContentFilters( query: string, searchFields: string[], matchType: MatchType = MatchType.PARTIAL ): Record<string, unknown> { return { filters: searchFields.map((field) => ({ attribute: { slug: field }, condition: matchType === MatchType.EXACT ? 'equals' : 'contains', value: query, })), matchAny: true, // Use OR logic to match any field }; } /** * Create basic name search filters */ protected createNameFilters( query: string, matchType: MatchType = MatchType.PARTIAL ): Record<string, unknown> { return { filters: [ { attribute: { slug: 'name' }, condition: matchType === MatchType.EXACT ? 'equals' : 'contains', value: query, }, ], }; } /** * Handle empty filters for pagination */ protected async handleEmptyFilters( searchFunction: ( filters: Record<string, unknown>, limit?: number, offset?: number ) => Promise<AttioRecord[]>, limit?: number, offset?: number ): Promise<AttioRecord[]> { try { return await searchFunction({ filters: [] }, limit, offset); } catch (error: unknown) { // If empty filters aren't supported, return empty array rather than failing const errorMessage = error instanceof Error ? error.message : 'Unknown error'; warn( 'BaseSearchStrategy', 'Search with empty filters failed, returning empty results', { errorMessage } ); return []; } } }

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