Skip to main content
Glama
lists.tsβ€’13.5 kB
/** * List operations for Attio * Handles list management and list entry operations */ import { getLazyAttioClient } from '../../api/lazy-client.js'; import { createScopedLogger } from '../../utils/logger.js'; import { AttioList, AttioListEntry, AttioListResponse, AttioSingleResponse, } from '../../types/attio.js'; import { callWithRetry, RetryConfig } from './retry.js'; import { ListEntryFilters } from './types.js'; import { processListEntries, transformFiltersToApiFormat, } from '../../utils/record-utils.js'; import { FilterValidationError } from '../../errors/api-errors.js'; import { SearchRequestBody, LogDetails, ValidationErrorDetails, ListErrorResponse, } from '../../types/api-operations.js'; /** * Gets all lists in the workspace * * @param objectSlug - Optional object type to filter lists by (e.g., 'companies', 'people') * @param limit - Maximum number of lists to fetch (default: 20) * @param retryConfig - Optional retry configuration * @returns Array of list objects */ export async function getAllLists( objectSlug?: string, limit: number = 20, retryConfig?: Partial<RetryConfig> ): Promise<AttioList[]> { const api = getLazyAttioClient(); let path = `/lists?limit=${limit}`; if (objectSlug) { path += `&objectSlug=${objectSlug}`; } return callWithRetry(async () => { const response = await api.get<AttioListResponse<AttioList>>(path); // Ensure we always return an array, never undefined/null/objects - handle multiple shape variants const items = Array.isArray(response?.data?.data) ? response.data.data : Array.isArray(response?.data?.lists) ? response.data.lists : Array.isArray(response?.data?.items) ? response.data.items : Array.isArray(response?.data) ? response.data : []; return items; }, retryConfig); } /** * Gets details for a specific list * * @param listId - The ID of the list * @param retryConfig - Optional retry configuration * @returns List details */ export async function getListDetails( listId: string, retryConfig?: Partial<RetryConfig> ): Promise<AttioList> { const api = getLazyAttioClient(); const path = `/lists/${listId}`; return callWithRetry(async () => { const response = await api.get<AttioSingleResponse<AttioList>>(path); return (response?.data?.data || response?.data) as AttioList; }, retryConfig); } /** * Gets entries in a list with pagination and filtering * * @param listId - The ID of the list * @param limit - Maximum number of entries to fetch * @param offset - Number of entries to skip * @param filters - Optional filters to apply * @param retryConfig - Optional retry configuration * @returns Array of list entries */ export async function getListEntries( listId: string, limit?: number, offset?: number, filters?: ListEntryFilters, retryConfig?: Partial<RetryConfig> ): Promise<AttioListEntry[]> { const api = getLazyAttioClient(); // Input validation - make sure we have a valid listId if (!listId) { throw new Error('Invalid list ID: No ID provided'); } // Coerce input parameters to ensure proper types const safeLimit = typeof limit === 'number' ? limit : undefined; const safeOffset = typeof offset === 'number' ? offset : undefined; // Create request body with parameters and filters const createRequestBody = async () => { // Start with base parameters const body: SearchRequestBody = { expand: ['record'], limit: safeLimit !== undefined ? safeLimit : 20, // Default to 20 if not specified offset: safeOffset !== undefined ? safeOffset : 0, // Default to 0 if not specified }; try { // Use our shared utility to transform filters to API format // Pass isListEntryContext=true since we're filtering list entries // Note: resourceType is undefined for list entries as they can contain mixed record types const filterObject = await transformFiltersToApiFormat( filters, true, true, undefined ); // Add filter to body if it exists if (filterObject.filter) { body.filter = filterObject.filter; // Log filter transformation for debugging in development if (process.env.NODE_ENV === 'development') { const log = createScopedLogger('lists.operations', 'getListEntries'); log.debug('Transformed filters', { originalFilters: JSON.stringify(filters), transformedFilters: JSON.stringify(filterObject.filter), useOrLogic: filters?.matchAny === true, filterCount: filters?.filters?.length || 0, }); } } } catch (err: unknown) { const error = err as Error; if (error instanceof FilterValidationError) { // Log the problematic filters for debugging if (process.env.NODE_ENV === 'development') { const log = createScopedLogger('lists.operations', 'getListEntries'); log.warn('Filter validation error', { error: error.message, providedFilters: JSON.stringify(filters), }); } // Rethrow with more context throw new Error(`Filter validation failed: ${error.message}`); } throw error; // Rethrow other errors } return body; }; // Enhanced logging function const logger = createScopedLogger('lists.operations', 'getListEntries'); const logOperation = ( stage: string, details?: LogDetails, isError = false ) => { if (process.env.NODE_ENV === 'development') { const data = { ...details, listId, limit: safeLimit, offset: safeOffset, hasFilters: filters && filters.filters ? filters.filters.length > 0 : false, timestamp: new Date().toISOString(), }; if (isError || stage.includes('failed')) { logger.warn(stage, data); } else { logger.debug(stage, data); } } }; // Call primary endpoint directly (Option A simplification) return callWithRetry(async () => { const path = `/lists/${listId}/entries/query`; const requestBody = await createRequestBody(); logOperation('Primary endpoint attempt', { path, requestBody: JSON.stringify(requestBody), }); const response = await api.post<AttioListResponse<AttioListEntry>>( path, requestBody ); logOperation('Primary endpoint successful', { resultCount: response.data?.data?.length || 0, }); return processListEntries(response.data?.data || []); }, retryConfig); } /** * Adds a record to a list * * @param listId - The ID of the list * @param recordId - The ID of the record to add * @param objectType - Optional object type ('companies', 'people', etc.) * @param initialValues - Optional initial values for the list entry (e.g., stage) * @param retryConfig - Optional retry configuration * @returns The created list entry */ export async function addRecordToList( listId: string, recordId: string, objectType?: string, initialValues?: Record<string, unknown>, retryConfig?: Partial<RetryConfig> ): Promise<AttioListEntry> { const api = getLazyAttioClient(); const path = `/lists/${listId}/entries`; // Input validation to ensure required parameters if (!listId || typeof listId !== 'string') { throw new Error('Invalid list ID: Must be a non-empty string'); } if (!recordId || typeof recordId !== 'string') { throw new Error('Invalid record ID: Must be a non-empty string'); } // Default object type to 'companies' if not specified const safeObjectType = objectType || 'companies'; return callWithRetry(async () => { const log = createScopedLogger('lists.operations', 'addRecordToList'); try { // Construct proper API payload according to Attio API requirements // The API expects parent_record_id, parent_object, and entry_values (required, even if empty) const payload = { data: { parent_record_id: recordId, parent_object: safeObjectType, // entry_values is required by the API, use empty object if no initial values provided entry_values: initialValues || {}, }, }; if (process.env.NODE_ENV === 'development') { log.info('Adding record to list', { path, listId, recordId, safeObjectType, initialValues: initialValues ?? null, payload, }); } const response = await api.post<AttioSingleResponse<AttioListEntry>>( path, payload ); if (process.env.NODE_ENV === 'development') { log.info('Add record success', { data: response.data }); } return response?.data?.data || response?.data; } catch (error: unknown) { const listError = error as ListErrorResponse; // Enhanced error logging with detailed information if (process.env.NODE_ENV === 'development') { log.warn('Add record error', { message: listError.message || 'Unknown error', status: listError.response?.status, data: listError.response?.data || {}, validationErrors: listError.response?.data?.validation_errors, }); } // Add more context to the error message if (listError.response?.status === 400) { const validationErrors = listError.response?.data?.validation_errors || []; const errorDetails = validationErrors .map( (e: ValidationErrorDetails) => `${e.path.join('.')}: ${e.message}` ) .join('; '); throw new Error( `Validation error adding record to list: ${ errorDetails || listError.message }` ); } // Let upstream handlers create specific, rich error objects. throw error; } }, retryConfig); } /** * Updates a list entry (e.g., changing stage) * * @param listId - The ID of the list * @param entryId - The ID of the list entry to update * @param attributes - The attributes to update (e.g., { stage: "Demo Scheduling" }) * @param retryConfig - Optional retry configuration * @returns The updated list entry */ export async function updateListEntry( listId: string, entryId: string, attributes: Record<string, unknown>, retryConfig?: Partial<RetryConfig> ): Promise<AttioListEntry> { const api = getLazyAttioClient(); const path = `/lists/${listId}/entries/${entryId}`; // Input validation if (!listId || typeof listId !== 'string') { throw new Error('Invalid list ID: Must be a non-empty string'); } if (!entryId || typeof entryId !== 'string') { throw new Error('Invalid entry ID: Must be a non-empty string'); } if ( !attributes || typeof attributes !== 'object' || Array.isArray(attributes) ) { throw new Error('Invalid attributes: Must be a non-empty object'); } return callWithRetry(async () => { const log = createScopedLogger('lists.operations', 'updateListEntry'); try { if (process.env.NODE_ENV === 'development') { log.info('Updating list entry', { path, listId, entryId, attributes }); } // Attio API expects updates to list entries in the 'data.entry_values' structure // This is specific to list entries, different from record updates in crud.ts const response = await api.patch<AttioSingleResponse<AttioListEntry>>( path, { data: { entry_values: attributes, }, } ); if (process.env.NODE_ENV === 'development') { log.info('Update list entry success', { data: response.data }); } return response?.data?.data || response?.data; } catch (error: unknown) { const updateError = error as ListErrorResponse; // Enhanced error logging with specific error types if (process.env.NODE_ENV === 'development') { log.warn('Update list entry error', { message: updateError.message || 'Unknown error', status: updateError.response?.status, data: updateError.response?.data || {}, }); } // Add more specific error types based on status codes if (updateError.response?.status === 404) { throw new Error(`List entry ${entryId} not found in list ${listId}`); } else if (updateError.response?.status === 400) { throw new Error( `Invalid attributes for list entry update: ${ updateError.response?.data?.message || 'Bad request' }` ); } else if (updateError.response?.status === 403) { throw new Error( `Insufficient permissions to update list entry ${entryId} in list ${listId}` ); } // Let upstream handlers create specific, rich error objects. throw error; } }, retryConfig); } /** * Removes a record from a list * * @param listId - The ID of the list * @param entryId - The ID of the list entry to remove * @param retryConfig - Optional retry configuration * @returns True if successful */ export async function removeRecordFromList( listId: string, entryId: string, retryConfig?: Partial<RetryConfig> ): Promise<boolean> { const api = getLazyAttioClient(); const path = `/lists/${listId}/entries/${entryId}`; return callWithRetry(async () => { await api.delete(path); return true; }, retryConfig); }

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