Skip to main content
Glama
QueryApiService.ts5.91 kB
/** * QueryApiService - Attio Query API operations for advanced search * * Issue #935: Extracted from UniversalSearchService.ts to reduce file size * Handles relationship, timeframe, and content searches using the Query API */ import { AttioRecord } from '@/types/attio.js'; import { UniversalResourceType } from '@/handlers/tool-configs/universal/types.js'; import { debug, createScopedLogger, OperationType } from '@/utils/logger.js'; import { AuthenticationError, AuthorizationError, NetworkError, RateLimitError, ServerError, ResourceNotFoundError, createApiErrorFromAxiosError, } from '@/errors/api-errors.js'; import { createRelationshipQuery, createTimeframeQuery, createContentSearchQuery, } from '@/utils/filters/index.js'; import { RelationshipQuery, TimeframeQuery } from '@/utils/filters/types.js'; import { getLazyAttioClient } from '@/api/lazy-client.js'; import * as AttioClientModule from '@/api/attio-client.js'; import type { AxiosInstance } from 'axios'; /** * Resolve Query API client (prefers mocked version in tests) */ function resolveQueryApiClient(): AxiosInstance { const mod = AttioClientModule as { getAttioClient?: () => AxiosInstance }; if (typeof mod.getAttioClient === 'function') { return mod.getAttioClient(); } return getLazyAttioClient(); } /** * Handle Query API errors consistently across methods * Issue #935: Extracted to reduce code duplication */ function handleQueryApiError( error: unknown, path: string, context: { resourceType: string; operation: string; metadata?: Record<string, unknown>; } ): AttioRecord[] { const apiError = createApiErrorFromAxiosError(error, path, 'POST'); // Re-throw critical errors that should bubble up if ( apiError instanceof AuthenticationError || apiError instanceof AuthorizationError || apiError instanceof NetworkError || apiError instanceof RateLimitError || apiError instanceof ServerError ) { throw apiError; } // Handle not found gracefully - return empty results if (apiError instanceof ResourceNotFoundError) { debug( 'QueryApiService', `No results for ${context.operation}`, context.metadata ); return []; } // Log and return empty for other errors createScopedLogger( 'QueryApiService', context.operation, OperationType.API_CALL ).error(`${context.operation} failed for ${context.resourceType}`, error); return []; } /** * Query API Service for advanced search operations */ export class QueryApiService { /** * Search records by relationship to another record */ static async searchByRelationship( sourceResourceType: UniversalResourceType, targetResourceType: UniversalResourceType, targetRecordId: string, limit?: number, offset?: number ): Promise<AttioRecord[]> { const relationshipQuery: RelationshipQuery = { sourceObjectType: sourceResourceType, targetObjectType: targetResourceType, targetAttribute: 'id', condition: 'equals', value: targetRecordId, }; const queryApiFilter = createRelationshipQuery(relationshipQuery); const path = `/objects/${sourceResourceType}/records/query`; try { const client = resolveQueryApiClient(); const requestBody = { ...queryApiFilter, limit: limit || 10, offset: offset || 0, }; const response = await client.post(path, requestBody); return response?.data?.data || []; } catch (error: unknown) { return handleQueryApiError(error, path, { resourceType: sourceResourceType, operation: 'searchByRelationship', metadata: { targetResourceType, targetRecordId }, }); } } /** * Search records within a specific timeframe */ static async searchByTimeframe( resourceType: UniversalResourceType, timeframeConfig: TimeframeQuery, limit?: number, offset?: number ): Promise<AttioRecord[]> { const queryApiFilter = createTimeframeQuery(timeframeConfig); const path = `/objects/${resourceType}/records/query`; try { const client = resolveQueryApiClient(); const requestBody = { ...queryApiFilter, limit: limit || 10, offset: offset || 0, }; const response = await client.post(path, requestBody); return response?.data?.data || []; } catch (error: unknown) { return handleQueryApiError(error, path, { resourceType, operation: 'searchByTimeframe', metadata: { timeframeConfig }, }); } } /** * Search records by content across multiple fields */ static async searchByContent( resourceType: UniversalResourceType, query: string, searchFields: string[] = [], useOrLogic: boolean = true, limit?: number, offset?: number ): Promise<AttioRecord[]> { let fields = searchFields; if (fields.length === 0) { switch (resourceType) { case UniversalResourceType.COMPANIES: fields = ['name', 'description', 'domains']; break; case UniversalResourceType.PEOPLE: fields = ['name', 'email_addresses', 'job_title']; break; default: fields = ['name']; break; } } const queryApiFilter = createContentSearchQuery(fields, query, useOrLogic); const path = `/objects/${resourceType}/records/query`; try { const client = resolveQueryApiClient(); const requestBody = { ...queryApiFilter, limit: limit || 10, offset: offset || 0, }; const response = await client.post(path, requestBody); return response?.data?.data || []; } catch (error: unknown) { return handleQueryApiError(error, path, { resourceType, operation: 'searchByContent', metadata: { query, fields }, }); } } }

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