Skip to main content
Glama
resource-mapping.tsβ€’9.14 kB
/** * Resource Mapping Utilities * * Handles mapping between universal resource types and Attio API paths. * Fixes the double-prefix issue for lists and provides consistent path generation. */ import { UniversalResourceType } from '../handlers/tool-configs/universal/types.js'; import { createScopedLogger, OperationType } from './logger.js'; const log = createScopedLogger( 'utils.resource-mapping', undefined, OperationType.SYSTEM ); /** * Resource path configuration */ export interface ResourcePathConfig { /** Base API path for the resource */ basePath: string; /** Singular form of the resource name */ singular: string; /** Plural form of the resource name */ plural: string; /** Whether this resource uses a special path structure */ customPath?: boolean; /** ID prefix used for this resource type */ idPrefix?: string; } /** * Mapping of resource types to their API path configurations */ const RESOURCE_PATH_MAP: Record<string, ResourcePathConfig> = { companies: { basePath: '/objects/companies', singular: 'company', plural: 'companies', idPrefix: 'comp_', }, people: { basePath: '/objects/people', singular: 'person', plural: 'people', idPrefix: 'pers_', }, deals: { basePath: '/objects/deals', singular: 'deal', plural: 'deals', idPrefix: 'deal_', }, tasks: { basePath: '/tasks', singular: 'task', plural: 'tasks', customPath: true, idPrefix: 'task_', }, lists: { basePath: '/lists', singular: 'list', plural: 'lists', customPath: true, idPrefix: 'list_', }, notes: { basePath: '/notes', singular: 'note', plural: 'notes', idPrefix: 'note_', }, records: { basePath: '/records', singular: 'record', plural: 'records', idPrefix: 'rec_', }, // Generic records fallback - should be avoided when possible objects: { basePath: '/objects', singular: 'object', plural: 'objects', }, }; /** * Resource mapping service */ export class ResourceMapper { /** * Get the API base path for a resource type */ static getBasePath(resourceType: string): string { const config = RESOURCE_PATH_MAP[resourceType.toLowerCase()]; if (!config) { // For unknown types, check if it's a custom object type // Custom objects typically use /objects/{type} format if (this.isCustomObjectType(resourceType)) { return `/objects/${resourceType.toLowerCase()}`; } // Default fallback for completely unknown types log.warn('Unknown resource type, using generic records path', { resourceType, }); return '/records'; } return config.basePath; } /** * Get the full API path for a specific resource record */ static getResourcePath(resourceType: string, recordId?: string): string { const basePath = this.getBasePath(resourceType); if (recordId) { return `${basePath}/${recordId}`; } return basePath; } /** * Get the search path for a resource type */ static getSearchPath(resourceType: string): string { const basePath = this.getBasePath(resourceType); // Lists use a different search endpoint if (resourceType.toLowerCase() === 'lists') { return '/lists'; } // Most resources append /query for search return `${basePath}/query`; } /** * Get the singular form of a resource type */ static getSingular(resourceType: string): string { const config = RESOURCE_PATH_MAP[resourceType.toLowerCase()]; return config?.singular || resourceType.toLowerCase(); } /** * Get the plural form of a resource type */ static getPlural(resourceType: string): string { const config = RESOURCE_PATH_MAP[resourceType.toLowerCase()]; return config?.plural || resourceType.toLowerCase(); } /** * Check if a resource type is a standard Attio type */ static isStandardType(resourceType: string): boolean { return resourceType.toLowerCase() in RESOURCE_PATH_MAP; } /** * Check if a resource type is likely a custom object type */ static isCustomObjectType(resourceType: string): boolean { // Standard types are not custom if (this.isStandardType(resourceType)) { return false; } // Custom object types typically: // - Don't contain special characters (except underscore) // - Are lowercase or have consistent casing // - Don't match reserved keywords const customObjectPattern = /^[a-z][a-z0-9_]*$/i; const reservedKeywords = ['api', 'v2', 'auth', 'webhooks', 'settings']; return ( customObjectPattern.test(resourceType) && !reservedKeywords.includes(resourceType.toLowerCase()) ); } /** * Normalize a resource type to its canonical form */ static normalizeResourceType(resourceType: string): string { const lowercased = resourceType.toLowerCase(); // Handle common variations const variations: Record<string, string> = { company: 'companies', person: 'people', deal: 'deals', task: 'tasks', list: 'lists', note: 'notes', record: 'records', object: 'objects', }; return variations[lowercased] || lowercased; } /** * Validate a resource type */ static isValidResourceType(resourceType: string): boolean { // Check if it's a universal resource type enum value if ( Object.values(UniversalResourceType).includes( resourceType as UniversalResourceType ) ) { return true; } // Check if it's a known standard type if (this.isStandardType(resourceType)) { return true; } // Check if it could be a custom object type if (this.isCustomObjectType(resourceType)) { return true; } return false; } /** * Get the ID prefix for a resource type */ static getIdPrefix(resourceType: string): string | undefined { const config = RESOURCE_PATH_MAP[resourceType.toLowerCase()]; return config?.idPrefix; } /** * Validate if an ID matches the expected format for a resource type */ static isValidIdFormat(resourceType: string, id: string): boolean { const prefix = this.getIdPrefix(resourceType); // If we have a known prefix, check if the ID starts with it if (prefix) { return id.startsWith(prefix); } // For unknown types, just check basic format // IDs should be alphanumeric with underscores and hyphens const genericIdPattern = /^[a-zA-Z0-9_-]+$/; return genericIdPattern.test(id); } /** * Get attributes path for a resource type */ static getAttributesPath(resourceType: string): string { // Attributes are typically at the object level, not record level const normalized = this.normalizeResourceType(resourceType); // Special handling for lists if (normalized === 'lists') { return '/lists/attributes'; } // Standard objects use /objects/{type}/attributes if (['companies', 'people', 'deals', 'tasks'].includes(normalized)) { return `/objects/${normalized}/attributes`; } // Custom objects if (this.isCustomObjectType(resourceType)) { return `/objects/${resourceType.toLowerCase()}/attributes`; } // Fallback return `/objects/attributes`; } /** * Build query parameters for list operations */ static buildListQueryParams(params: { limit?: number; offset?: number; sort_by?: string; sort_order?: 'asc' | 'desc'; [key: string]: unknown; }): URLSearchParams { const queryParams = new URLSearchParams(); // Add pagination params if (params.limit !== undefined) { queryParams.append('limit', String(params.limit)); } if (params.offset !== undefined) { queryParams.append('offset', String(params.offset)); } // Add sorting params if (params.sort_by) { queryParams.append('sort_by', params.sort_by); } if (params.sort_order) { queryParams.append('sort_order', params.sort_order); } // Add any other params for (const [key, value] of Object.entries(params)) { if ( !['limit', 'offset', 'sort_by', 'sort_order'].includes(key) && value !== undefined ) { if (typeof value === 'object') { queryParams.append(key, JSON.stringify(value)); } else { queryParams.append(key, String(value)); } } } return queryParams; } } /** * Helper function to get resource path (for backward compatibility) */ export function getResourcePath( resourceType: string, recordId?: string ): string { return ResourceMapper.getResourcePath(resourceType, recordId); } /** * Helper function to get search path (for backward compatibility) */ export function getSearchPath(resourceType: string): string { return ResourceMapper.getSearchPath(resourceType); } /** * Helper function to normalize resource type (for backward compatibility) */ export function normalizeResourceType(resourceType: string): string { return ResourceMapper.normalizeResourceType(resourceType); }

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