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);
}