Skip to main content
Glama
field-suggestions.tsβ€’8.92 kB
/** * Field suggestion utilities for improved error messages * Provides fuzzy matching and suggestion capabilities for field names, * resource types, and enum values */ /** * Calculate Levenshtein distance between two strings * Used for fuzzy matching to find similar field names */ export function levenshteinDistance(str1: string, str2: string): number { const matrix: number[][] = []; // Initialize first row and column for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } // Fill in the matrix for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, // substitution matrix[i][j - 1] + 1, // insertion matrix[i - 1][j] + 1 // deletion ); } } } return matrix[str2.length][str1.length]; } /** * Find the most similar strings from a list of valid options * @param input - The incorrect input string * @param validOptions - List of valid options to match against * @param maxSuggestions - Maximum number of suggestions to return (default: 3) * @param threshold - Maximum distance threshold for suggestions (default: 3) * @returns Array of similar options sorted by similarity */ export function findSimilarOptions( input: string, validOptions: string[], maxSuggestions: number = 3, threshold: number = 3 ): string[] { if (!input || !validOptions.length) { return []; } const normalizedInput = input.toLowerCase().trim(); // Calculate distances for all options const distances = validOptions.map((option) => ({ option, distance: levenshteinDistance(normalizedInput, option.toLowerCase()), })); // Filter by threshold and sort by distance const suggestions = distances .filter((d) => d.distance <= threshold) .sort((a, b) => a.distance - b.distance) .slice(0, maxSuggestions) .map((d) => d.option); // If no suggestions found with Levenshtein, try substring matching if (suggestions.length === 0) { const substringMatches = validOptions.filter( (option) => option.toLowerCase().includes(normalizedInput) || normalizedInput.includes(option.toLowerCase()) ); return substringMatches.slice(0, maxSuggestions); } return suggestions; } /** * Generate a helpful error message with field suggestions * @param fieldName - The incorrect field name provided * @param validFields - List of valid field names * @param context - Additional context (e.g., resource type) * @returns Enhanced error message with suggestions */ export function generateFieldSuggestionMessage( fieldName: string, validFields: string[], context?: string ): string { const suggestions = findSimilarOptions(fieldName, validFields); let message = `Invalid field name: "${fieldName}"`; if (context) { message += ` for ${context}`; } if (suggestions.length > 0) { message += `. Did you mean: ${suggestions .map((s) => `"${s}"`) .join(', ')}?`; } else if (validFields.length > 0) { // Show first few valid fields if no close matches found const preview = validFields.slice(0, 5); message += `. Valid fields include: ${preview .map((f) => `"${f}"`) .join(', ')}`; if (validFields.length > 5) { message += ` (and ${validFields.length - 5} more)`; } } return message; } /** * Generate error message for invalid enum/select values * @param value - The invalid value provided * @param validValues - List of valid enum values * @param fieldName - Name of the field * @returns Enhanced error message with valid options */ export function generateEnumSuggestionMessage( value: string | number | boolean, validValues: string[], fieldName: string ): string { const valueStr = String(value); const suggestions = findSimilarOptions(valueStr, validValues, 2, 2); let message = `Invalid value "${valueStr}" for field "${fieldName}"`; if (suggestions.length > 0) { message += `. Did you mean: ${suggestions .map((s) => `"${s}"`) .join(' or ')}?`; } // Always show all valid options for enums (usually limited set) if (validValues.length <= 10) { message += ` Valid options are: ${validValues .map((v) => `"${v}"`) .join(', ')}.`; } else { // For large sets, show a subset const preview = validValues.slice(0, 5); message += ` Valid options include: ${preview .map((v) => `"${v}"`) .join(', ')} (and ${validValues.length - 5} more).`; } return message; } /** * Generate error message for read-only field attempts * @param fieldName - Name of the read-only field * @param operation - The attempted operation (create, update) * @returns Clear error message about read-only status */ export function generateReadOnlyFieldMessage( fieldName: string, operation: 'create' | 'update' = 'update' ): string { return `Field "${fieldName}" is read-only and cannot be ${ operation === 'create' ? 'set during creation' : 'modified' }. This field is automatically managed by the system.`; } /** * Generate error message for invalid resource types * @param resourceType - The invalid resource type provided * @param validTypes - List of valid resource types * @returns Enhanced error message with valid types */ export function generateResourceTypeSuggestionMessage( resourceType: string, validTypes: string[] ): string { const suggestions = findSimilarOptions(resourceType, validTypes, 3, 5); // Increase threshold for resource types let message = `Invalid resource type: "${resourceType}"`; if (suggestions.length > 0) { message += `. Did you mean: ${suggestions .map((s) => `"${s}"`) .join(', ')}?`; } message += ` Valid resource types are: ${validTypes .map((t) => `"${t}"`) .join(', ')}.`; return message; } /** * Known Attio resource types for validation */ export const VALID_RESOURCE_TYPES = [ 'people', 'companies', 'deals', 'workspaces', 'users', 'lists', 'objects', 'records', 'tasks', 'notes', 'comments', 'threads', 'entries', 'attributes', 'webhooks', ]; /** * Common field name mappings for better suggestions * Maps common incorrect names to correct Attio field names */ export const FIELD_NAME_MAPPINGS: Record<string, string> = { // Common person field mappings firstname: 'first_name', first_name: 'first_name', // Support hyphen/space variations lastname: 'last_name', last_name: 'last_name', // Support hyphen/space variations fullname: 'name', phone: 'phone_numbers', email: 'email_addresses', company: 'primary_company', title: 'job_title', position: 'job_title', // Common company field mappings company_name: 'name', website: 'domain', employees: 'employee_count', size: 'company_size', // Common timestamp mappings created: 'created_at', updated: 'updated_at', modified: 'updated_at', date_created: 'created_at', date_updated: 'updated_at', // Common ID mappings person_id: 'id', company_id: 'id', record_id: 'id', object_id: 'id', }; /** * Check if a field name might be a common mistake and suggest correction * @param fieldName - The field name to check * @returns The suggested correct field name or null */ export function getMappedFieldName(fieldName: string): string | null { const normalized = fieldName.toLowerCase().replace(/[- ]/g, '_'); return FIELD_NAME_MAPPINGS[normalized] || null; } /** * Enhanced field validation with helpful suggestions * @param fieldName - Field name to validate * @param validFields - List of valid field names * @param readOnlyFields - List of read-only field names * @param operation - The operation being performed * @returns Validation result with error message if invalid */ export function validateFieldWithSuggestions( fieldName: string, validFields: string[], readOnlyFields: string[] = [], operation: 'create' | 'update' = 'update' ): { valid: boolean; error?: string } { // Check if field exists if (!validFields.includes(fieldName)) { // Check for common mapping const mappedName = getMappedFieldName(fieldName); if (mappedName && validFields.includes(mappedName)) { return { valid: false, error: `Invalid field name: "${fieldName}". Did you mean "${mappedName}"?`, }; } // Generate suggestion message return { valid: false, error: generateFieldSuggestionMessage(fieldName, validFields), }; } // Check if field is read-only if (readOnlyFields.includes(fieldName)) { return { valid: false, error: generateReadOnlyFieldMessage(fieldName, operation), }; } return { valid: true }; }

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