Skip to main content
Glama
attribute-format-helpers.tsβ€’13.8 kB
/** * Attribute format helpers to convert common user mistakes to correct API formats * * This module provides automatic format conversion for common attribute mistakes * to improve user experience and reduce errors. */ import { createScopedLogger } from './logger.js'; // Type definitions for better type safety interface PersonalNameObject { first_name?: string; last_name?: string; full_name: string; } interface RelationshipReference { target_object: string; target_record_id: string; } interface LegacyRelationshipReference { record_id: string; } type EmailItem = string | { email_address: string }; type PhoneItem = string | { phone_number?: string; number?: string }; type RelationshipItem = | string | RelationshipReference | LegacyRelationshipReference; const logger = createScopedLogger( 'utils.format-helpers', 'attribute-format-helpers' ); /** * Converts common attribute format mistakes to correct API format * * @param resourceType - The type of resource (companies, people, etc.) * @param attributes - The attributes object with potential format issues * @returns Corrected attributes object */ export function convertAttributeFormats( resourceType: string, attributes: Record<string, unknown> ): Record<string, unknown> { let corrected = { ...attributes }; switch (resourceType) { case 'companies': corrected = convertCompanyAttributes(corrected); break; case 'people': corrected = convertPeopleAttributes(corrected); break; case 'deals': corrected = convertDealAttributes(corrected); break; } return corrected; } /** * Converts company attribute formats */ function convertCompanyAttributes( attributes: Record<string, unknown> ): Record<string, unknown> { const corrected = { ...attributes }; // Convert 'domain' to 'domains' array if ('domain' in corrected && !('domains' in corrected)) { corrected.domains = Array.isArray(corrected.domain) ? corrected.domain : [corrected.domain]; delete corrected.domain; try { logger.debug("Converted 'domain' to 'domains' array"); } catch { // Logger not available, continue silently } } // Ensure domains is always an array if (corrected.domains && !Array.isArray(corrected.domains)) { corrected.domains = [corrected.domains]; try { logger.debug('Converted domains to array format'); } catch { // Logger not available, continue silently } } // Handle common typos if ('typpe' in corrected && !('type' in corrected)) { corrected.type = corrected.typpe; delete corrected.typpe; try { logger.debug("Fixed typo: 'typpe' -> 'type'"); } catch { // Logger not available, continue silently } } return corrected; } /** * Converts deal attribute formats */ function convertDealAttributes( attributes: Record<string, unknown> ): Record<string, unknown> { const corrected = { ...attributes }; // Convert associated_company to array format: string -> [{ target_object, target_record_id }] if ('associated_company' in corrected) { const value = corrected.associated_company; if (typeof value === 'string') { corrected.associated_company = [ { target_object: 'companies', target_record_id: value }, ]; try { logger.debug('Converted associated_company string to object array'); } catch { // Logger not available, continue silently } } else if (Array.isArray(value)) { corrected.associated_company = (value as RelationshipItem[]).map( (v: RelationshipItem) => { if (typeof v === 'string') { return { target_object: 'companies', target_record_id: v }; } else if ( v && typeof v === 'object' && 'record_id' in v && !('target_record_id' in v) ) { // Convert { record_id: "..." } to { target_object: "companies", target_record_id: "..." } const legacyRef = v as LegacyRelationshipReference; return { target_object: 'companies', target_record_id: legacyRef.record_id, }; } else if ( v && typeof v === 'object' && 'target_record_id' in v && !('target_object' in v) ) { // HOTFIX: Add missing target_object to incomplete relationship reference const incompleteRef = v as { target_record_id: string }; return { target_object: 'companies', target_record_id: incompleteRef.target_record_id, }; } else { return v; // Already in correct format or unknown format } } ); try { logger.debug('Converted associated_company array entries'); } catch { // Logger not available, continue silently } } } // Convert associated_people to array format: string -> [{ target_object, target_record_id }] if ('associated_people' in corrected) { const value = corrected.associated_people; if (typeof value === 'string') { corrected.associated_people = [ { target_object: 'people', target_record_id: value }, ]; try { logger.debug('Converted associated_people string to object array'); } catch { // Logger not available, continue silently } } else if (Array.isArray(value)) { corrected.associated_people = (value as RelationshipItem[]).map( (v: RelationshipItem) => { if (typeof v === 'string') { return { target_object: 'people', target_record_id: v }; } else if ( v && typeof v === 'object' && 'record_id' in v && !('target_record_id' in v) ) { // Convert { record_id: "..." } to { target_object: "people", target_record_id: "..." } const legacyRef = v as LegacyRelationshipReference; return { target_object: 'people', target_record_id: legacyRef.record_id, }; } else if ( v && typeof v === 'object' && 'target_record_id' in v && !('target_object' in v) ) { // HOTFIX: Add missing target_object to incomplete relationship reference const incompleteRef = v as { target_record_id: string }; return { target_object: 'people', target_record_id: incompleteRef.target_record_id, }; } else { return v; // Already in correct format or unknown format } } ); try { logger.debug('Converted associated_people array entries'); } catch { // Logger not available, continue silently } } } return corrected; } /** * Converts people attribute formats */ function convertPeopleAttributes( attributes: Record<string, unknown> ): Record<string, unknown> { const corrected = { ...attributes }; // Handle name conversion to personal-name array format if ( corrected.name || corrected.first_name || corrected.last_name || corrected.full_name ) { const nameObj: Record<string, string> = {}; // Handle string name input (e.g., "Jane Smith") if (typeof corrected.name === 'string') { const parts = corrected.name.trim().split(' '); if (parts.length >= 2) { nameObj.first_name = parts[0]; nameObj.last_name = parts.slice(1).join(' '); } else { nameObj.first_name = parts[0] || ''; nameObj.last_name = ''; } nameObj.full_name = corrected.name; try { logger.debug('Parsed string name into components', { name: corrected.name, }); } catch { // Logger not available, continue silently } } // Handle individual name components if (corrected.first_name) { nameObj.first_name = String(corrected.first_name); } if (corrected.last_name) { nameObj.last_name = String(corrected.last_name); } if (corrected.full_name) { nameObj.full_name = String(corrected.full_name); } // Ensure full_name is always present for Attio's personal-name validation if (!nameObj.full_name) { nameObj.full_name = [nameObj.first_name, nameObj.last_name] .filter(Boolean) .join(' '); } // Create name field as ARRAY in personal-name format expected by Attio corrected.name = [nameObj]; // Remove the flattened fields since they're now in the name object delete corrected.first_name; delete corrected.last_name; delete corrected.full_name; try { logger.debug('Created personal-name array', { name: corrected.name }); } catch { // Logger not available, continue silently } } // Convert email_addresses from object format to string array if (corrected.email_addresses && Array.isArray(corrected.email_addresses)) { const converted = (corrected.email_addresses as EmailItem[]).map( (item: EmailItem) => { if (typeof item === 'object' && item.email_address) { try { logger.debug('Converting email object format to string'); } catch { // Logger not available, continue silently } return (item as { email_address: string }).email_address; } return item; } ); corrected.email_addresses = converted; } // Ensure email_addresses is always an array if (corrected.email_addresses && !Array.isArray(corrected.email_addresses)) { corrected.email_addresses = [corrected.email_addresses]; try { logger.debug('Converted email_addresses to array format'); } catch { // Logger not available, continue silently } } // Convert phone_numbers from object format to string array if (corrected.phone_numbers && Array.isArray(corrected.phone_numbers)) { const converted = (corrected.phone_numbers as PhoneItem[]).map( (item: PhoneItem) => { if (typeof item === 'object' && (item.phone_number || item.number)) { try { logger.debug('Converting phone object format to string'); } catch { // Logger not available, continue silently } const phoneObj = item as { phone_number?: string; number?: string }; return phoneObj.phone_number || phoneObj.number; } return item; } ); corrected.phone_numbers = converted; } return corrected; } /** * Validates people attributes before POST to ensure correct Attio format * Throws validation errors if required formats are not met */ export function validatePeopleAttributesPrePost( attributes: Record<string, unknown> ): void { // Validate name format if present if (attributes.name) { if (!Array.isArray(attributes.name as unknown)) { throw new Error('People name must be an array of personal-name objects'); } const nameArray = attributes.name as PersonalNameObject[]; if (nameArray.length > 0 && !nameArray[0].full_name) { throw new Error('People name[0].full_name must be a non-empty string'); } if (nameArray.length > 0 && typeof nameArray[0].full_name !== 'string') { throw new Error('People name[0].full_name must be a string'); } if (nameArray.length > 0 && nameArray[0].full_name.trim() === '') { throw new Error('People name[0].full_name must be a non-empty string'); } } // Validate email_addresses format if present if (attributes.email_addresses) { if (!Array.isArray(attributes.email_addresses as unknown)) { throw new Error('People email_addresses must be an array of strings'); } for (const email of attributes.email_addresses as string[]) { if (typeof email !== 'string') { throw new Error('All email_addresses must be strings, not objects'); } } } } /** * Generates helpful error message with correct format examples */ export function getFormatErrorHelp( resourceType: string, attributeName: string, error: string ): string { const examples: Record<string, Record<string, string>> = { companies: { domains: ` Correct format for 'domains': - domains: ["example.com", "www.example.com"] - NOT: domain: "example.com" - NOT: domains: "example.com" (must be array) Note: Use 'domains' (plural) to avoid creating duplicate companies!`, type: ` The 'type' field requires a valid select option from your workspace. Common values might include: "Customer", "Partner", "Prospect", etc. Check your Attio workspace for valid options.`, }, people: { name: ` Correct format for 'name': - name: "John Doe" - name: "Jane Smith" - NOT: name: {first_name: "John", last_name: "Doe"} - NOT: name: {firstName: "John", lastName: "Doe"} The name field should be a simple string, not an object.`, email_addresses: ` Correct format for 'email_addresses': - email_addresses: ["user@example.com", "alt@example.com"] - NOT: email_addresses: [{email_address: "user@example.com"}] - NOT: email_addresses: "user@example.com" (must be array)`, phone_numbers: ` Correct format for 'phone_numbers': - phone_numbers: ["+1234567890", "+0987654321"] - NOT: phone_numbers: [{phone_number: "+1234567890"}]`, company: ` Correct format for 'company' (record reference): - company: "company_id_here" - company: {record_id: "company_id_here"} - NOT: company: "Company Name" (use ID, not name)`, }, }; const helpText = examples[resourceType]?.[attributeName]; if (helpText) { return `${error}\n${helpText}`; } return 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