Skip to main content
Glama
AttributeAwareNormalizer.ts5.21 kB
import { PhoneValidationError, toE164, validatePhoneNumber, } from './PhoneNormalizer.js'; import { ErrorType, UniversalValidationError, } from '@/handlers/tool-configs/universal/errors/validation-errors.js'; interface PhoneValidationIssue { fieldPath: string; error: PhoneValidationError; } function recordPhoneIssue( issues: PhoneValidationIssue[], fieldPath: string, error: PhoneValidationError ) { issues.push({ fieldPath, error }); } function normalizeSinglePhoneValue( rawValue: unknown, fieldPath: string, issues: PhoneValidationIssue[] ): string | null { if (typeof rawValue !== 'string') { recordPhoneIssue( issues, fieldPath, new PhoneValidationError( 'INVALID_TYPE', 'Phone numbers must be provided as strings.', String(rawValue) ) ); return null; } const validation = validatePhoneNumber(rawValue); if (validation.valid && validation.e164) { return validation.e164; } if (validation.error) { recordPhoneIssue(issues, fieldPath, validation.error); } else { recordPhoneIssue( issues, fieldPath, new PhoneValidationError( 'INVALID_FORMAT', 'Phone number format is not recognized.', rawValue ) ); } return null; } /** * Normalize phone number structure and format * Transforms {phone_number: X} to {original_phone_number: X} and applies E.164 formatting */ function normalizePhoneNumbers( value: unknown, fieldPath: string, issues: PhoneValidationIssue[] ): unknown { if (!Array.isArray(value)) { if (typeof value === 'string') { const normalized = normalizeSinglePhoneValue(value, fieldPath, issues); return normalized ?? value; } if (value && typeof value === 'object') { // Normalize single phone object; reuse array flow for consistency const result = normalizePhoneNumbers([value], fieldPath, issues); const firstItem = Array.isArray(result) ? (result as Record<string, unknown>[])[0] : result; return firstItem ?? value; } // Non-string scalars (booleans, numbers) previously bypassed normalization; keep passthrough return value; } return value.map((item) => { // Handle object with wrong key: {phone_number: "+1...", label: "work"} → {original_phone_number: "+1...", label: "work"} if ( item && typeof item === 'object' && !Array.isArray(item) && 'phone_number' in item ) { const itemObj = item as Record<string, unknown>; const { phone_number, ...otherFields } = itemObj; const normalized = normalizeSinglePhoneValue( phone_number, `${fieldPath}.phone_number`, issues ); return { ...otherFields, original_phone_number: normalized || phone_number, }; } // Handle object with correct key: {original_phone_number: "+1...", label: "work"} if ( item && typeof item === 'object' && !Array.isArray(item) && 'original_phone_number' in item ) { const itemObj = item as Record<string, unknown>; const { original_phone_number, ...otherFields } = itemObj; const normalized = normalizeSinglePhoneValue( original_phone_number, `${fieldPath}.original_phone_number`, issues ); return { ...otherFields, original_phone_number: normalized || original_phone_number, }; } // Handle direct string format if (typeof item === 'string') { const normalized = normalizeSinglePhoneValue( item, `${fieldPath}[]`, issues ); return { original_phone_number: normalized || item }; } // Pass through other formats unchanged (e.g., booleans) return item; }); } export async function normalizeValues( resourceType: string, values: Record<string, unknown>, _attributes?: string[] ) { const out: Record<string, unknown> = { ...values }; const phoneIssues: PhoneValidationIssue[] = []; for (const [k, v] of Object.entries(values)) { const isPhoney = /phone/.test(k); // fast path if you don't want to fetch schemas if (isPhoney) { out[k] = normalizePhoneNumbers(v, k, phoneIssues); } } if (phoneIssues.length > 0) { const formattedIssues = phoneIssues.map((issue) => { const sanitizedInput = issue.error.input.trim() || 'empty input'; return `${issue.fieldPath}: ${issue.error.message} (received "${sanitizedInput}")`; }); const headline = phoneIssues.length > 1 ? `Phone number validation failed (${phoneIssues.length} issues):` : 'Phone number validation failed:'; const details = phoneIssues.length > 1 ? `${headline}\n${formattedIssues.join('\n')}` : `${headline} ${formattedIssues[0]}`; throw new UniversalValidationError(details, ErrorType.USER_ERROR, { field: phoneIssues.length === 1 ? phoneIssues[0]?.fieldPath : 'phone_numbers', suggestion: 'Provide phone numbers in E.164 format, for example +15551234567.', example: '+15551234567', cause: phoneIssues[0]?.error, }); } return out; }

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