Skip to main content
Glama
index.ts23 kB
/** * Record-related functionality */ import { getLazyAttioClient } from '../../api/lazy-client.js'; import { createRecord, getRecord, updateRecord, deleteRecord, listRecords, batchCreateRecords, batchUpdateRecords, BatchConfig, BatchResponse, } from '../../api/operations/index.js'; import { ResourceType, AttioRecord, RecordAttributes, RecordListParams, } from '../../types/attio.js'; import { getErrorStatus, getErrorMessage, HttpErrorLike, } from '../../types/error-interfaces.js'; /** * Creates a new record for a specific object type * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param attributes - Record attributes as key-value pairs * @param objectId - Optional object ID (alternative to slug) * @returns Created record */ export async function createObjectRecord<T extends AttioRecord>( objectSlug: string | ResourceType, attributes: RecordAttributes, objectId?: string ): Promise<T> { // Ensure objectSlug is a string value, not undefined if (!objectSlug) { throw new Error( '[createObjectRecord] Object slug is required for creating records' ); } // Normalize objectSlug to ensure proper type handling const normalizedSlug = typeof objectSlug === 'string' ? objectSlug : String(objectSlug); // Add debug logging (includes E2E mode) if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); const log = createScopedLogger('objects.records', 'createObjectRecord'); log.info('Creating record', { objectSlug: normalizedSlug }); log.debug('Attributes', { attributes }); } try { // Use the core API function if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').info( 'Calling createRecord with', { objectSlug: normalizedSlug, objectId, attributes } ); } const result = await createRecord<T>({ objectSlug: normalizedSlug, objectId, attributes, }); if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').info( 'createRecord returned', { hasId: !!result?.id, hasValues: !!result?.values, resultType: typeof result, isEmptyObject: result && Object.keys(result).length === 0, } ); } // Check for empty results and apply fallback query for companies const isEmpty = !result || (typeof result === 'object' && Object.keys(result).length === 0); const hasNoValidId = !result?.id?.record_id && !result?.record_id; if ( (isEmpty || hasNoValidId) && normalizedSlug === 'companies' && attributes?.name ) { // Extract the actual name value from the Attio format const nameValue = typeof attributes.name === 'object' && attributes.name !== null && 'value' in attributes.name ? (attributes.name as { value: string }).value : String(attributes.name); if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').warn( 'Empty result for company creation, trying query fallback', { nameValue } ); } try { const api = getLazyAttioClient(); // Use the documented query endpoint with exact name match const queryResponse = await api.post( `/objects/companies/records/query`, { filter: { name: nameValue }, limit: 1, } ); if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').debug( 'Query fallback response', { queryResponse: queryResponse?.data, hasData: !!queryResponse?.data?.data, dataLength: Array.isArray(queryResponse?.data?.data) ? queryResponse.data.data.length : 'not array', } ); } // If we found an existing record, return it if ( queryResponse?.data?.data && Array.isArray(queryResponse.data.data) && queryResponse.data.data.length > 0 ) { const foundRecord = queryResponse.data.data[0]; if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import( '../../utils/logger.js' ); createScopedLogger('objects.records', 'createObjectRecord').info( 'Found existing company via query fallback', { foundRecord } ); } return foundRecord as T; } } catch (queryError) { if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').warn( 'Query fallback failed', { error: queryError instanceof Error ? queryError.message : String(queryError), } ); } // Continue with original empty result rather than throwing } } return result; } catch (error: unknown) { if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').warn( 'Primary createRecord failed, trying fallback', { error: error instanceof Error ? error.message : String(error) } ); } // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } else if (typeof error === 'string') { throw new Error(error); } // Fallback implementation in case the core function fails try { const api = getLazyAttioClient(); const path = `/objects/${objectId || objectSlug}/records`; // ENHANCED DEBUG: Add path builder logging as requested by user const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').debug( 'POST', { path, sampleKeys: Object.keys(attributes || {}) } ); // Use the same payload format as the main implementation const body = { data: { values: attributes, }, }; if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').debug( 'Fallback request', { path, payloadSize: JSON.stringify(body).length } ); } try { const response = await api.post(path, body); if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const { createScopedLogger } = await import('../../utils/logger.js'); createScopedLogger('objects.records', 'createObjectRecord').debug( 'Fallback response structure', { hasData: !!response?.data, hasNestedData: !!response?.data?.data, dataKeys: response?.data ? Object.keys(response.data) : [], nestedDataKeys: response?.data?.data ? Object.keys(response.data.data) : [], } ); } // Extract the result with proper error handling const result = response?.data?.data || response?.data; // Check for empty or invalid responses, but allow legitimate create responses const resultIsObject = result !== null && typeof result === 'object' && !Array.isArray(result); const recordCandidate = resultIsObject ? (result as Record<string, unknown>) : undefined; const looksLikeCreatedRecord = !!recordCandidate && (typeof (recordCandidate.id as Record<string, unknown> | undefined) ?.record_id === 'string' || typeof recordCandidate.record_id === 'string' || 'web_url' in recordCandidate || 'created_at' in recordCandidate); if ( !result || (resultIsObject && Object.keys(recordCandidate ?? {}).length === 0 && !looksLikeCreatedRecord) ) { throw new Error( `Create operation returned empty or invalid response. Response structure: ${JSON.stringify(response?.data)}` ); } return result; } catch (err: unknown) { const status = getErrorStatus(err); const messageFallback = getErrorMessage(err) ?? ''; const msg = String( (typeof err === 'object' && err !== null ? (err as HttpErrorLike).response?.data?.error?.message : undefined) || messageFallback ); const isDuplicateDomain = status === 422 && /domain/i.test(msg) && /(taken|unique|already)/i.test(msg); if ( process.env.E2E_MODE === 'true' && isDuplicateDomain && normalizedSlug === 'companies' ) { // Mutate domain once and retry for E2E tests const suffix = Math.random().toString(36).slice(2, 6); if ( body.data.values?.domain && typeof body.data.values.domain === 'string' ) { body.data.values.domain = `${body.data.values.domain.replace(/\.$/, '')}-${suffix}`; } else if ( Array.isArray(body.data.values?.domains) && body.data.values.domains[0] && typeof body.data.values.domains[0] === 'string' ) { body.data.values.domains[0] = body.data.values.domains[0].replace(/\.$/, '') + `-${suffix}`; } await new Promise((r) => setTimeout(r, 150 + Math.floor(Math.random() * 200)) ); // jitter 150–350ms const retryResponse = await api.post(path, body); return retryResponse?.data?.data || retryResponse?.data; } throw err; } } catch (fallbackError) { throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)); } } } /** * Gets details for a specific record * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param recordId - ID of the record to retrieve * @param attributes - Optional list of attribute slugs to include * @param objectId - Optional object ID (alternative to slug) * @returns Record details */ export async function getObjectRecord<T extends AttioRecord>( objectSlug: string, recordId: string, attributes?: string[], objectId?: string ): Promise<T> { try { // Use the core API function return await getRecord<T>(objectSlug, recordId, attributes, objectId); } catch (error: unknown) { // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } // Fallback implementation in case the core function fails try { const api = getLazyAttioClient(); let path = `/objects/${objectId || objectSlug}/records/${recordId}`; // Add attributes parameter if provided if (attributes && attributes.length > 0) { const attributesParam = attributes.join(','); path += `?attributes=${encodeURIComponent(attributesParam)}`; } const response = await api.get(path); return response?.data?.data || response?.data; } catch (fallbackError) { throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)); } } } /** * Updates a specific record * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param recordId - ID of the record to update * @param attributes - Record attributes to update * @param objectId - Optional object ID (alternative to slug) * @returns Updated record */ export async function updateObjectRecord<T extends AttioRecord>( objectSlug: string, recordId: string, attributes: RecordAttributes, objectId?: string ): Promise<T> { try { // Use the core API function return await updateRecord<T>({ objectSlug, objectId, recordId, attributes, }); } catch (error: unknown) { // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } // Fallback implementation in case the core function fails try { const api = getLazyAttioClient(); const path = `/objects/${objectId || objectSlug}/records/${recordId}`; const response = await api.patch(path, { attributes, }); // Add null guards to prevent undefined → {} conversion if (!response || !response.data) { throw { status: 500, body: { code: 'invalid_response', message: `Invalid API response for record update: ${recordId}`, }, }; } const result = response.data.data || response.data; // Check for empty object results that indicate API errors if ( !result || (typeof result === 'object' && Object.keys(result).length === 0) ) { throw { status: 404, body: { code: 'not_found', message: `Record with ID "${recordId}" not found for update.`, }, }; } return result; } catch (fallbackError) { throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)); } } } /** * Deletes a specific record * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param recordId - ID of the record to delete * @param objectId - Optional object ID (alternative to slug) * @returns True if deletion was successful */ export async function deleteObjectRecord( objectSlug: string, recordId: string, objectId?: string ): Promise<boolean> { try { // Use the core API function return await deleteRecord(objectSlug, recordId, objectId); } catch (error: unknown) { // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } // Fallback implementation in case the core function fails try { const api = getLazyAttioClient(); const path = `/objects/${objectId || objectSlug}/records/${recordId}`; const response = await api.delete(path); // Add null guards to prevent undefined → {} conversion if (!response) { throw { status: 500, body: { code: 'invalid_response', message: `Invalid API response for record deletion: ${recordId}`, }, }; } // DELETE operations typically return empty response on success // Check if response indicates failure (non-2xx status would be caught by axios) return true; } catch (fallbackError) { throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)); } } } /** * Lists records for a specific object type with filtering options * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param options - Optional listing options (pagination, filtering, etc.) * @param objectId - Optional object ID (alternative to slug) * @returns Array of records */ export async function listObjectRecords<T extends AttioRecord>( objectSlug: string, options: Omit<RecordListParams, 'objectSlug' | 'objectId'> = {}, objectId?: string ): Promise<T[]> { try { // Use the core API function return await listRecords<T>({ objectSlug, objectId, ...options, }); } catch (error: unknown) { // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } // Fallback implementation in case the core function fails try { const api = getLazyAttioClient(); // Build query parameters const queryParams = new URLSearchParams(); if (options.page) { queryParams.append('page', String(options.page)); } if (options.pageSize) { queryParams.append('pageSize', String(options.pageSize)); } if (options.query) { queryParams.append('query', options.query); } if (options.attributes && options.attributes.length > 0) { queryParams.append('attributes', options.attributes.join(',')); } if (options.sort) { queryParams.append('sort', options.sort); } if (options.direction) { queryParams.append('direction', options.direction); } const path = `/objects/${objectId || objectSlug}/records${ queryParams.toString() ? '?' + queryParams.toString() : '' }`; const response = await api.get(path); return response.data.data || []; } catch (fallbackError) { throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)); } } } /** * Creates multiple records in a batch operation * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param records - Array of record attributes to create * @param objectId - Optional object ID (alternative to slug) * @param batchConfig - Optional batch configuration * @returns Batch response with created records */ export async function batchCreateObjectRecords<T extends AttioRecord>( objectSlug: string, records: RecordAttributes[], objectId?: string, batchConfig?: Partial<BatchConfig> ): Promise<BatchResponse<T>> { try { // Map records to the expected format const recordItems = records.map((attributes) => ({ attributes })); // Use the core API function const createdRecords = await batchCreateRecords<T>( { objectSlug, objectId, records: recordItems, }, batchConfig?.retryConfig ); // Format as batch response return { results: createdRecords.map((record) => ({ success: true, data: record, })), summary: { total: records.length, succeeded: createdRecords.length, failed: records.length - createdRecords.length, }, }; } catch (error: unknown) { // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } // Fallback implementation - execute each creation individually const results: BatchResponse<T> = { results: [], summary: { total: records.length, succeeded: 0, failed: 0, }, }; // Process each record individually await Promise.all( records.map(async (recordAttributes, index) => { try { const record = await createObjectRecord<T>( objectSlug, recordAttributes, objectId ); results.results.push({ id: `create_record_${index}`, success: true, data: record, }); results.summary.succeeded++; } catch (createError) { results.results.push({ id: `create_record_${index}`, success: false, error: createError, }); results.summary.failed++; } }) ); return results; } } /** * Updates multiple records in a batch operation * * @param objectSlug - Object slug (e.g., 'companies', 'people') * @param records - Array of records with IDs and attributes to update * @param objectId - Optional object ID (alternative to slug) * @param batchConfig - Optional batch configuration * @returns Batch response with updated records */ export async function batchUpdateObjectRecords<T extends AttioRecord>( objectSlug: string, records: Array<{ id: string; attributes: RecordAttributes }>, objectId?: string, batchConfig?: Partial<BatchConfig> ): Promise<BatchResponse<T>> { try { // Use the core API function const updatedRecords = await batchUpdateRecords<T>( { objectSlug, objectId, records, }, batchConfig?.retryConfig ); // Format as batch response return { results: updatedRecords.map((record, index) => ({ id: records[index].id, success: true, data: record, })), summary: { total: records.length, succeeded: updatedRecords.length, failed: records.length - updatedRecords.length, }, }; } catch (error: unknown) { // If it's an error from the original implementation, just pass it through if (error instanceof Error) { throw error; } // Fallback implementation - execute each update individually const results: BatchResponse<T> = { results: [], summary: { total: records.length, succeeded: 0, failed: 0, }, }; // Process each record individually await Promise.all( records.map(async (record) => { try { const updatedRecord = await updateObjectRecord<T>( objectSlug, record.id, record.attributes, objectId ); results.results.push({ id: record.id, success: true, data: updatedRecord, }); results.summary.succeeded++; } catch (updateError) { results.results.push({ id: record.id, success: false, error: updateError, }); results.summary.failed++; } }) ); return results; } } // Re-export formatting utilities export { formatRecordAttribute, formatRecordAttributes } from './formatters.js';

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