Skip to main content
Glama
base-operations.tsβ€’10.5 kB
/** * Base operations for all Attio objects with dynamic field detection */ import { formatAllAttributes } from '../api/attribute-types.js'; import { createObjectRecord, updateObjectRecord, deleteObjectRecord, } from './records/index.js'; import { ResourceType, AttioRecord } from '../types/attio.js'; import { getAttributeSlug } from '../utils/attribute-mapping/index.js'; import { createScopedLogger, OperationType } from '../utils/logger.js'; type AttributeMap = Record<string, unknown>; type AttributeValidator< Raw extends AttributeMap, Validated extends AttributeMap, > = (attrs: Raw) => Promise<Validated> | Validated; type UpdateValidator< Raw extends AttributeMap, Validated extends AttributeMap, > = (id: string, attrs: Raw) => Promise<Validated> | Validated; function isRecordObject(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null; } function hasRecordIdentity(record: Record<string, unknown>): boolean { const idValue = record.id as Record<string, unknown> | undefined; if (idValue && typeof idValue.record_id === 'string') { return true; } if (typeof record.record_id === 'string') { return true; } if ('web_url' in record || 'created_at' in record) { return true; } return false; } /** * Translates all attribute names in a record using the attribute mapping system * * @param objectType - The type of object (companies, people, etc.) * @param attributes - Raw attributes object with user-friendly names * @returns Attributes object with API-compatible attribute names */ function translateAttributeNames( objectType: ResourceType, attributes: Record<string, unknown> ): Record<string, unknown> { const translated: Record<string, unknown> = {}; const logger = createScopedLogger( 'objects/base-operations', 'translateAttributeNames', OperationType.TRANSFORMATION ); for (const [userKey, value] of Object.entries(attributes)) { // Translate the attribute name using the mapping system const apiKey = getAttributeSlug(userKey, objectType); // Log the translation in development mode if (process.env.NODE_ENV === 'development' && userKey !== apiKey) { logger.debug('Mapped attribute name', { objectType, from: userKey, to: apiKey, }); } translated[apiKey] = value; } return translated; } /** * Creates a new object record with dynamic field formatting * * @param objectType - The type of object (companies, people, etc.) * @param attributes - Raw attributes for object creation * @param validator - Optional validator function * @returns Created object record */ export async function createObjectWithDynamicFields< T extends AttioRecord, RawAttrs extends AttributeMap = AttributeMap, ValidatedAttrs extends AttributeMap = RawAttrs, >( objectType: ResourceType, attributes: RawAttrs, validator?: AttributeValidator<RawAttrs, ValidatedAttrs> ): Promise<T> { // Validate if validator provided const validatedAttributes = ( validator ? await validator(attributes) : attributes ) as ValidatedAttrs; // Translate attribute names using the mapping system (e.g., "website" -> "domains") const mappedAttributes = translateAttributeNames( objectType, validatedAttributes as AttributeMap ); // Use dynamic field type detection to format attributes correctly const transformedAttributes = await formatAllAttributes( objectType, mappedAttributes ); // Debug log to help diagnose issues (includes E2E mode) if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { const logger = createScopedLogger( 'objects/base-operations', 'createObjectWithDynamicFields', OperationType.TRANSFORMATION ); logger.debug('Original attributes', { attributes: validatedAttributes }); logger.debug('Mapped attributes', { attributes: mappedAttributes }); logger.debug('Final transformed attributes', { attributes: transformedAttributes, }); } try { // Create the object const result = await createObjectRecord<T>( objectType, transformedAttributes ); if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { createScopedLogger( 'objects/base-operations', 'createObjectWithDynamicFields', OperationType.DATA_PROCESSING ).debug('Result from createObjectRecord', { hasId: !!result?.id, hasValues: !!result?.values, resultType: typeof result, isEmptyObject: result && Object.keys(result).length === 0, }); } // Additional check for empty objects that might slip through, but allow legitimate create responses const looksLikeCreatedRecord = result && isRecordObject(result) && hasRecordIdentity(result); if ( !result || (isRecordObject(result) && Object.keys(result).length === 0 && !looksLikeCreatedRecord) ) { // For companies, allow empty results to pass through to createObjectRecord fallback logic if (objectType === ResourceType.COMPANIES) { if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { createScopedLogger( 'objects/base-operations', 'createObjectWithDynamicFields', OperationType.DATA_PROCESSING ).warn( 'Empty result detected, passing to createObjectRecord fallback' ); } return result; // Let createObjectRecord handle the fallback } throw new Error( `Create operation returned empty result for ${objectType}` ); } return result; } catch (error: unknown) { createScopedLogger( 'objects/base-operations', 'createObjectWithDynamicFields', OperationType.API_CALL ).error('Error creating record', error); throw error; } } /** * Updates an existing object record with dynamic field formatting * * @param objectType - The type of object (companies, people, etc.) * @param recordId - ID of the record to update * @param attributes - Raw attributes to update * @param validator - Optional validator function * @returns Updated object record */ export async function updateObjectWithDynamicFields< T extends AttioRecord, RawAttrs extends AttributeMap = AttributeMap, ValidatedAttrs extends AttributeMap = RawAttrs, >( objectType: ResourceType, recordId: string, attributes: RawAttrs, validator?: UpdateValidator<RawAttrs, ValidatedAttrs> ): Promise<T> { // Validate if validator provided const validatedAttributes = ( validator ? await validator(recordId, attributes) : attributes ) as ValidatedAttrs; // Translate attribute names using the mapping system (e.g., "website" -> "domains") const mappedAttributes = translateAttributeNames( objectType, validatedAttributes as AttributeMap ); // Use dynamic field type detection to format attributes correctly const transformedAttributes = await formatAllAttributes( objectType, mappedAttributes ); if (process.env.NODE_ENV === 'development') { const logger = createScopedLogger( 'objects/base-operations', 'updateObjectWithDynamicFields', OperationType.TRANSFORMATION ); logger.debug('Original attributes', { attributes: validatedAttributes }); logger.debug('Mapped attributes', { attributes: mappedAttributes }); logger.debug('Final transformed attributes', { attributes: transformedAttributes, }); } // Update the object const result = await updateObjectRecord<T>( objectType, recordId, transformedAttributes ); // Additional check for empty objects that might slip through // For companies, allow empty results to pass through since updateRecord has fallback logic if ( !result || (typeof result === 'object' && Object.keys(result).length === 0) ) { // For companies, the updateRecord function has fallback logic that should handle empty responses if (objectType === ResourceType.COMPANIES) { if ( process.env.NODE_ENV === 'development' || process.env.E2E_MODE === 'true' ) { createScopedLogger( 'objects/base-operations', 'updateObjectWithDynamicFields', OperationType.DATA_PROCESSING ).warn( 'Empty result detected for update, allowing fallback logic to handle' ); } // The fallback should have been handled in updateRecord, if we still get empty result, something is wrong if (!result) { throw new Error( `Update operation returned null result for ${objectType} record: ${recordId}` ); } // Empty object might be a valid result from fallback, return it return result; } throw new Error( `Update operation returned empty result for ${objectType} record: ${recordId}` ); } return result; } /** * Updates a specific attribute of an object with dynamic field formatting * * @param objectType - The type of object (companies, people, etc.) * @param recordId - ID of the record to update * @param attributeName - Name of the attribute to update * @param attributeValue - New value for the attribute * @param updateFn - The update function to use * @returns Updated object record */ export async function updateObjectAttributeWithDynamicFields< T extends AttioRecord, AttrMap extends AttributeMap = AttributeMap, >( objectType: ResourceType, recordId: string, attributeName: string, attributeValue: unknown, updateFn: (id: string, attrs: AttrMap) => Promise<T> ): Promise<T> { // Update the specific attribute using the provided update function const attributes = { [attributeName]: attributeValue } as AttrMap; return await updateFn(recordId, attributes); } /** * Deletes an object record * * @param objectType - The type of object (companies, people, etc.) * @param recordId - ID of the record to delete * @param validator - Optional validator function * @returns True if deletion was successful */ export async function deleteObjectWithValidation( objectType: ResourceType, recordId: string, validator?: (id: string) => void ): Promise<boolean> { // Validate if validator provided if (validator) { validator(recordId); } // Delete the object return await deleteObjectRecord(objectType, recordId); }

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