field-mapping-utils.ts6.7 KBView on GitHub
/**
 * Shared types and utilities for CRM field mappings
 * Used by both provider-config-card.tsx and crm-integration-card.tsx
 *
 * Note: Provider configuration (connection types, fields, instructions) are now
 * exclusively defined in server-side drivers and fetched via integrations.list API.
 */

// ============================================
// Field Mapping Types and Utilities
// ============================================

// Common date-fns format options
export const DATE_FORMAT_OPTIONS = [
  { value: 'yyyy-MM-dd', example: '2025-10-10' },
  { value: 'MM/dd/yyyy', example: '10/10/2025' },
  { value: 'M/d/yyyy', example: '1/7/2025' },
  { value: 'MM/dd', example: '10/10' },
  { value: 'M/d', example: '1/7' },
  { value: 'dd/MM/yyyy', example: '10/10/2025' },
  { value: 'MMM dd, yyyy', example: 'Oct 10, 2025' },
  { value: 'MMMM dd, yyyy', example: 'October 10, 2025' },
  { value: 'yyyy-MM-dd HH:mm', example: '2025-10-10 14:30' },
  { value: 'MM/dd/yy', example: '10/10/25' },
  { value: 'M/d/yy', example: '1/7/25' },
  { value: 'dd-MM-yyyy', example: '10-10-2025' },
  { value: 'yyyy/MM/dd', example: '2025/10/10' },
  { value: 'EEEE, MMMM dd, yyyy', example: 'Friday, October 10, 2025' },
] as const;

// Field mapping types for CRM integrations
export type FieldMapping =
  | { crmFieldName: string; cedarFieldName: string; allowedValues?: string[] }
  | {
      crmFieldName: string;
      template: string;
      prefix?: string;
      transformations?: Record<string, { type: 'date' | 'number' | 'string'; format?: string }>;
      allowedValues?: string[];
    };

// Type guards for field mappings
export const isSimpleMapping = (
  mapping: FieldMapping,
): mapping is { crmFieldName: string; cedarFieldName: string; allowedValues?: string[] } => {
  return 'cedarFieldName' in mapping;
};

/**
 * Returns true if the mapping references `fieldKey` — for simple mappings this
 * is an exact cedarFieldName match; for complex/template mappings we check both
 * the transformations key map and the template interpolation literal so that
 * stale entries can be found and removed even when cedarFieldName is absent.
 */
export const matchesField = (mapping: FieldMapping, fieldKey=[redacted] boolean => {
  if (isSimpleMapping(mapping)) return mapping.cedarFieldName === fieldKey;
  return !!(mapping.transformations?.[fieldKey] || mapping.template.includes(`{${fieldKey}}`));
};

export const isComplexMapping = (
  mapping: FieldMapping,
): mapping is {
  crmFieldName: string;
  template: string;
  prefix?: string;
  transformations?: Record<string, { type: 'date' | 'number' | 'string'; format?: string }>;
  allowedValues?: string[];
} => {
  return 'template' in mapping;
};

/**
 * Extract allowed values from picklist data
 * Handles both Salesforce format (string[]) and HubSpot format ({label, value}[])
 * Returns the API values that should be used for validation
 */
export function extractPicklistValues(
  picklist: string[] | Array<{ label: string; value: string }> | undefined,
): string[] | undefined {
  if (!picklist || picklist.length === 0) return undefined;
  // Check if first item is an object with 'value' property (HubSpot format)
  if (typeof picklist[0] === 'object' && picklist[0] !== null && 'value' in picklist[0]) {
    return (picklist as Array<{ label: string; value: string }>).map((item) => item.value);
  }
  // Otherwise it's a string array (Salesforce format)
  return picklist as string[];
}

/**
 * Extract picklist options with both value and label for Select components
 * HubSpot format: {label, value} → use value as the API value, label for display
 * Salesforce format: plain strings → use original string as both value and label
 *                    (Salesforce API expects the exact label like "New Business", not normalized)
 */
export function extractPicklistOptions(
  picklist: string[] | Array<{ label: string; value: string }> | undefined,
): Array<{ value: string; label: string }> | undefined {
  if (!picklist || picklist.length === 0) return undefined;

  // Check if first item is an object with 'value' property (HubSpot format)
  if (typeof picklist[0] === 'object' && picklist[0] !== null && 'value' in picklist[0]) {
    return (picklist as Array<{ label: string; value: string }>).map((item) => ({
      value: item.value,
      label: item.label,
    }));
  }

  // Salesforce format: plain strings - use original value for both
  // Salesforce API expects the exact label value (e.g., "New Business" not "new_business")
  return (picklist as string[]).map((item) => ({
    value: item,
    label: item,
  }));
}

/**
 * Normalize raw mapping data from API/metadata into typed FieldMapping objects
 * Uses defensive validation to ensure transformation types are valid
 */
export function normalizeFieldMappings(rawMappings: Array<unknown>): FieldMapping[] {
  return rawMappings
    .map((m: unknown) => {
      if (typeof m === 'object' && m !== null) {
        const mapping = m as Record<string, unknown>;
        if ('template' in mapping) {
          // Complex mapping
          const transformations = mapping.transformations as
            | Record<string, { type: string; format?: string }>
            | undefined;
          const normalizedTransformations:
            | Record<string, { type: 'date' | 'number' | 'string'; format?: string }>
            | undefined = transformations ? {} : undefined;
          if (transformations) {
            for (const [key, value] of Object.entries(transformations)) {
              if (value && typeof value === 'object' && 'type' in value) {
                const type = value.type;
                if (type === 'date' || type === 'number' || type === 'string') {
                  normalizedTransformations![key] = { type, format: value.format };
                }
              }
            }
          }
          const allowedValues = Array.isArray(mapping.allowedValues)
            ? mapping.allowedValues.map((v) => String(v))
            : undefined;
          return {
            crmFieldName: String(mapping.crmFieldName),
            template: String(mapping.template),
            prefix: mapping.prefix ? String(mapping.prefix) : undefined,
            transformations: normalizedTransformations,
            allowedValues,
          } as FieldMapping;
        } else if ('cedarFieldName' in mapping) {
          // Simple mapping
          const allowedValues = Array.isArray(mapping.allowedValues)
            ? mapping.allowedValues.map((v) => String(v))
            : undefined;
          return {
            crmFieldName: String(mapping.crmFieldName),
            cedarFieldName: String(mapping.cedarFieldName),
            allowedValues,
          } as FieldMapping;
        }
      }
      return null;
    })
    .filter((m): m is FieldMapping => m !== null);
}