Skip to main content
Glama
attio-create.service.ts.backupβ€’14.5 kB
/** * AttioCreateService - Real API implementation * * Pure real API implementation with no environment checks. * Uses getAttioClient with rawE2E option for consistent API communication. */ import type { CreateService } from './types.js'; import type { AttioRecord } from '../../types/attio.js'; import { getAttioClient } from '../../api/attio-client.js'; import { EnhancedApiError } from '../../errors/enhanced-api-errors.js'; import { debug, error as logError } from '../../utils/logger.js'; import { extractRecordId } from '../../utils/validation/uuid-validation.js'; import { extractAttioRecord, looksLikeCreatedRecord, assertLooksLikeCreated, isTestRun, debugRecordShape, } from './extractor.js'; import { normalizeCompanyValues, normalizePersonValues, convertTaskToAttioRecord, normalizeEmailsToObjectFormat, normalizeEmailsToStringFormat, } from './data-normalizers.js'; /** * Real API implementation of CreateService * * Handles company, person, task, and note creation through the Attio API. * Includes data normalization, error recovery, and robust error handling. * * @example * ```typescript * const service = new AttioCreateService(); * const company = await service.createCompany({ * name: "Acme Corp", * domain: "acme.com" * }); * ``` */ export class AttioCreateService implements CreateService { /** * Creates a company record with domain normalization * * @param input - Company data including name, domain/domains, industry, etc. * @returns Promise<AttioRecord> - Created company record with id.record_id * * @example * ```typescript * // Single domain * const company = await service.createCompany({ * name: "Tech Corp", * domain: "techcorp.com" * }); * * // Multiple domains * const company = await service.createCompany({ * name: "Multi Corp", * domains: ["multi.com", "multicorp.io"] * }); * ``` */ async createCompany(input: Record<string, unknown>): Promise<AttioRecord> { const client = getAttioClient({ rawE2E: true }); // Normalize company domains to string array const normalizedCompany = normalizeCompanyValues(input); const payload = { data: { values: normalizedCompany, }, }; debug('AttioCreateService', 'πŸ” EXACT API PAYLOAD', { url: '/objects/companies/records', payload: JSON.stringify(payload, null, 2), }); try { const response = await client.post('/objects/companies/records', payload); debug('AttioCreateService', 'Company API response', { status: response?.status, statusText: response?.statusText, hasData: !!response?.data, hasNestedData: !!response?.data?.data, dataKeys: response?.data ? Object.keys(response.data) : [], }); let record = extractAttioRecord(response); // Enrich missing id from web_url if available if (record && (!record as any || !(record as any).id || !(record as any).id?.record_id)) { const webUrl = (record as any)?.web_url || (response?.data as any)?.web_url; const rid = webUrl ? extractRecordId(String(webUrl)) : undefined; if (rid) { (record as any).id = { ...(record as any).id, record_id: rid }; } } // Handle empty response with recovery attempt const mustRecover = !record || !(record as any).id || !(record as any).id?.record_id; if (mustRecover) { record = await this.recoverCompanyRecord(client, normalizedCompany); } assertLooksLikeCreated(record, 'AttioCreateService.createCompany'); if (isTestRun()) { debug( 'AttioCreateService', 'Normalized company record', debugRecordShape(record) ); } return record as AttioRecord; } catch (err: any) { const status = err?.response?.status; const errorData = err?.response?.data; logError('AttioCreateService', 'API Error Details', { status, errorBody: errorData, requestPayload: payload, }); throw this.enhanceApiError( err, 'createCompany', '/objects/companies/records', 'companies' ); } } /** * Creates a person record with name and email normalization * * @param input - Person data including name, email/email_addresses, title, etc. * @returns Promise<AttioRecord> - Created person record with id.record_id * * @example * ```typescript * // String name and email * const person = await service.createPerson({ * name: "John Doe", * email: "john@example.com" * }); * * // Multiple emails * const person = await service.createPerson({ * name: "Jane Smith", * email_addresses: ["jane@company.com", "jane.smith@company.com"] * }); * * // Complex name object * const person = await service.createPerson({ * name: { first_name: "Bob", last_name: "Wilson" }, * email: "bob@example.com", * job_title: "Senior Engineer" * }); * ``` */ async createPerson(input: Record<string, unknown>): Promise<AttioRecord> { const client = getAttioClient({ rawE2E: true }); const filteredPersonData = normalizePersonValues(input); debug('AttioCreateService', 'πŸ” EXACT API PAYLOAD', { url: '/objects/people/records', payload: JSON.stringify({ data: { values: filteredPersonData } }, null, 2), }); try { let response = await this.createPersonWithRetry( client, filteredPersonData ); debug('AttioCreateService', 'Person API response', { status: response?.status, statusText: response?.statusText, hasData: !!response?.data, hasNestedData: !!response?.data?.data, }); let record = extractAttioRecord(response); // Enrich missing id from web_url if available if (record && (!record as any || !(record as any).id || !(record as any).id?.record_id)) { const webUrl = (record as any)?.web_url || (response?.data as any)?.web_url; const rid = webUrl ? extractRecordId(String(webUrl)) : undefined; if (rid) { (record as any).id = { ...(record as any).id, record_id: rid }; } } // Handle empty response with recovery attempt const mustRecover = !record || !(record as any).id || !(record as any).id?.record_id; if (mustRecover) { record = await this.recoverPersonRecord(client, filteredPersonData); } assertLooksLikeCreated(record, 'AttioCreateService.createPerson'); if (isTestRun()) { debug( 'AttioCreateService', 'Normalized person record', debugRecordShape(record) ); } return record as AttioRecord; } catch (err: any) { const status = err?.response?.status; const errorData = err?.response?.data; const payload = { data: { values: filteredPersonData } }; // Define payload here for error logging logError('AttioCreateService', 'API Error Details', { status, errorBody: errorData, requestPayload: payload, }); throw this.enhanceApiError( err, 'createPerson', '/objects/people/records', 'people' ); } } async createTask(input: Record<string, unknown>): Promise<AttioRecord> { // Delegate to the tasks object for now, this will be refactored later const { createTask } = await import('../../objects/tasks.js'); const createdTask = await createTask(input.content as string, { assigneeId: input.assigneeId as string, dueDate: input.dueDate as string, recordId: input.recordId as string, }); // Convert task to AttioRecord format return convertTaskToAttioRecord(createdTask, input); } async updateTask( taskId: string, input: Record<string, unknown> ): Promise<AttioRecord> { // Delegate to the tasks object for now, this will be refactored later const { updateTask } = await import('../../objects/tasks.js'); const updatedTask = await updateTask(taskId, { content: input.content as string, status: input.status as string, assigneeId: input.assigneeId as string, dueDate: input.dueDate as string, recordIds: input.recordIds as string[], }); // Convert task to AttioRecord format return convertTaskToAttioRecord(updatedTask, input); } async createNote(input: { resource_type: string; record_id: string; title: string; content: string; format?: string; }): Promise<any> { // Always use real API here; factory determines mock usage. const { createNote } = await import('../../objects/notes.js'); const { unwrapAttio, normalizeNote } = await import('../../utils/attio-response.js'); const noteData = { parent_object: input.resource_type, parent_record_id: input.record_id, title: input.title, content: input.content, format: (input.format as 'markdown' | 'plaintext') || 'plaintext', }; const response = await createNote(noteData); // Unwrap varying API envelopes and normalize to stable shape const attioNote = unwrapAttio<any>(response); return normalizeNote(attioNote); } async listNotes(params: { resource_type?: string; record_id?: string; }): Promise<unknown[]> { // Use real API calls for notes listing const { listNotes } = await import('../../objects/notes.js'); const query = { parent_object: params.resource_type, parent_record_id: params.record_id, }; const response = await listNotes(query); return response.data || []; } // Private helper methods private async createPersonWithRetry( client: any, filteredPersonData: Record<string, unknown> ) { const doCreate = async (values: Record<string, unknown>) => client.post('/objects/people/records', { data: { values } }); try { // Attempt #1 return await doCreate(filteredPersonData); } catch (firstErr: unknown) { const error = firstErr as { response?: { status?: number } }; const status = error?.response?.status; // Only retry on 400 with alternate email schema if (status === 400) { const alt: Record<string, unknown> = { ...filteredPersonData }; const emails = alt.email_addresses as unknown[] | undefined; if (emails && emails.length) { if (typeof emails[0] === 'string') { alt.email_addresses = normalizeEmailsToObjectFormat(emails); } else if ( emails[0] && typeof emails[0] === 'object' && emails[0] !== null && 'email_address' in emails[0] ) { alt.email_addresses = normalizeEmailsToStringFormat(emails); } return await doCreate(alt); } } throw firstErr; } } private async recoverCompanyRecord( client: any, normalizedCompany: Record<string, unknown> ) { // Recovery: try to find the created company by unique fields const domain = Array.isArray(normalizedCompany.domains) ? normalizedCompany.domains[0] : undefined; try { if (domain) { const { data: searchByDomain } = await client.post( '/objects/companies/records/search', { filter: { domains: { contains: domain } }, limit: 1, order: { created_at: 'desc' }, } ); const rec = extractAttioRecord(searchByDomain); if (rec?.id?.record_id) return rec; } const name = normalizedCompany.name as string; if (name) { const { data: searchByName } = await client.post( '/objects/companies/records/search', { filter: { name: { eq: name } }, limit: 1, order: { created_at: 'desc' }, } ); const rec = extractAttioRecord(searchByName); if (rec?.id?.record_id) return rec; } } catch (e) { debug('AttioCreateService', 'Company recovery failed', { message: (e as Error)?.message, }); } throw new EnhancedApiError( 'Attio createCompany returned an empty/invalid record payload', 500, '/objects/companies/records', 'POST', { httpStatus: 500, resourceType: 'companies', operation: 'create', } ); } private async recoverPersonRecord( client: any, filteredPersonData: Record<string, unknown> ) { // Recovery: try to find the created person by primary email const email = Array.isArray(filteredPersonData.email_addresses) ? (filteredPersonData.email_addresses[0] as string) : undefined; try { if (email) { const { data: search } = await client.post( '/objects/people/records/search', { filter: { email_addresses: { contains: email } }, limit: 1, order: { created_at: 'desc' }, } ); const rec = extractAttioRecord(search); if (rec?.id?.record_id) return rec; } } catch (e) { debug('AttioCreateService', 'Person recovery failed', { message: (e as Error)?.message, }); } throw new EnhancedApiError( 'Attio createPerson returned an empty/invalid record payload', 500, '/objects/people/records', 'POST', { httpStatus: 500, resourceType: 'people', operation: 'create', } ); } private enhanceApiError( err: unknown, operation: string, endpoint: string, resourceType: string ) { const error = err as { response?: { status?: number; data?: unknown }; message?: string; name?: string }; const status = error?.response?.status ?? 500; const data = error?.response?.data; logError('AttioCreateService', `${operation} Direct API error`, { status, data, }); let msg: string; if (status === 500) { msg = `invalid request: Attio ${operation} failed with a server error.`; } else if (status && data) { msg = `Attio ${operation} failed (${status}): ${JSON.stringify(data)}`; } else { msg = error?.message || `${operation} error`; } return new EnhancedApiError(msg, status, endpoint, 'POST', { httpStatus: status, resourceType, operation, originalError: err as Error, }); } }

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