UNPKG

n8n-nodes-close-crm

Version:

N8N community node for Close CRM integration with comprehensive lead, opportunity, task, note, call management, and enhanced triggers for workflow automation

1,225 lines (1,224 loc) 87.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.customActivityCustomFieldsLoadMethods = exports.customActivityCustomFieldsUpdateSections = exports.customActivityCustomFieldsCreateSections = exports.customFieldValidators = exports.customFieldsLoadMethods = exports.customFieldsUpdateSections = exports.customFieldsCreateSections = void 0; exports.constructCustomFieldsPayload = constructCustomFieldsPayload; exports.constructContactCustomFieldsPayload = constructContactCustomFieldsPayload; exports.getCachedCustomActivityCustomFields = getCachedCustomActivityCustomFields; exports.constructCustomActivityCustomFieldsPayload = constructCustomActivityCustomFieldsPayload; const GenericFunctions_1 = require("../GenericFunctions"); /** * Cache for storing custom field definitions with workspace isolation * Structure: Map<workspaceId, { timestamp: number, fields: CustomField[] }> */ const customFieldsCache = new Map(); /** * Cache for storing user lists with workspace isolation * Structure: Map<workspaceId, { timestamp: number, users: INodePropertyOptions[] }> */ const usersCache = new Map(); /** * Cache TTL in milliseconds (10 minutes for fields, 15 minutes for users) */ const FIELD_CACHE_TTL = 10 * 60 * 1000; // 10 minutes const USER_CACHE_TTL = 15 * 60 * 1000; // 15 minutes /** * Get workspace ID for cache isolation */ async function getWorkspaceId(context) { var _a; // Use credentials hash or organization ID for workspace isolation // Fallback to 'default' if no specific identifier available const credentials = await ((_a = context.getCredentials) === null || _a === void 0 ? void 0 : _a.call(context, 'closeApi')); return (credentials === null || credentials === void 0 ? void 0 : credentials.apiKey) ? Buffer.from(credentials.apiKey).toString('base64') : 'default'; } /** * Check if cache is still valid */ function isCacheValid(timestamp, ttl) { return Date.now() - timestamp < ttl; } /** * Normalize various possible Close API responses into an array of CustomField items * Supports shapes like: { data: [...] }, [ { data: [...], has_more: false } ], or an array of fields */ function normalizeCustomFieldsResponse(raw) { if (!raw) return []; // If already an array of fields if (Array.isArray(raw)) { if (raw.length === 0) return []; // Some wrappers return an array of objects with a .data array inside if (raw[0] && Array.isArray(raw[0].data)) { return raw.flatMap((entry) => (Array.isArray(entry.data) ? entry.data : [])); } // Assume it's already the list of fields return raw; } // Object with data property if (raw.data && Array.isArray(raw.data)) { return raw.data; } // Single field object fallback if (raw.id && raw.type) { return [raw]; } return []; } /** * Custom Fields UI sections for Create operation */ exports.customFieldsCreateSections = [ { displayName: 'Custom Fields', name: 'customFields', type: 'collection', placeholder: 'Add Custom Field', default: {}, displayOptions: { show: { resource: ['lead', 'opportunity'], operation: ['create'], }, }, description: 'Add custom field values', options: [ { displayName: 'Text Field', name: 'textField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add text custom fields', options: [ { name: 'textFields', displayName: 'Text Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getTextFields', }, default: '', description: 'Select the text field', }, { displayName: 'Value', name: 'fieldValue', type: 'string', default: '', description: 'Enter the text value', }, ], }, ], }, { displayName: 'Number Field', name: 'numberField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add number custom fields', options: [ { name: 'numberFields', displayName: 'Number Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getNumberFields', }, default: '', description: 'Select the number field', }, { displayName: 'Value', name: 'fieldValue', type: 'number', default: 0, description: 'Enter the numeric value', }, ], }, ], }, { displayName: 'Date Field', name: 'dateField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add date custom fields', options: [ { name: 'dateFields', displayName: 'Date Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getDateFields', }, default: '', description: 'Select the date field', }, { displayName: 'Value', name: 'fieldValue', type: 'dateTime', default: '', description: 'Select the date value', }, ], }, ], }, { displayName: 'Choice Field (Single)', name: 'choiceSingleField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add single-choice custom fields', options: [ { name: 'choiceSingleFields', displayName: 'Single Choice Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getSingleChoiceFields', }, default: '', description: 'Select the choice field', }, { displayName: 'Value', name: 'fieldValue', type: 'options', typeOptions: { loadOptionsMethod: 'getAllChoiceValues', }, default: '', description: 'Select a value (shows all possible values from all choice fields)', displayOptions: { hide: { fieldId: [''], }, }, }, ], }, ], }, { displayName: 'Choice Field (Multiple)', name: 'choiceMultipleField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add multiple-choice custom fields', options: [ { name: 'choiceMultipleFields', displayName: 'Multiple Choice Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getMultipleChoiceFields', }, default: '', description: 'Select the choice field', }, { displayName: 'Action', name: 'action', type: 'options', options: [ { name: 'Replace', value: 'replace', description: 'Replace all existing values' }, { name: 'Add', value: 'add', description: 'Add a value to the existing list' }, { name: 'Remove', value: 'remove', description: 'Remove a value from the existing list', }, ], default: 'replace', description: 'Whether to replace, add to, or remove from the existing values', }, { displayName: 'Values', name: 'fieldValues', type: 'multiOptions', typeOptions: { loadOptionsMethod: 'getAllChoiceValues', }, default: [], description: 'Select multiple values (shows all possible values from all choice fields)', displayOptions: { hide: { fieldId: [''], }, }, }, ], }, ], }, { displayName: 'User Field (Single)', name: 'userSingleField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add single-user custom fields', options: [ { name: 'userSingleFields', displayName: 'Single User Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getSingleUserFields', }, default: '', description: 'Select the user field', }, { displayName: 'User', name: 'fieldValue', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers', }, default: '', description: 'Select a user from the list', }, ], }, ], }, { displayName: 'User Field (Multiple)', name: 'userMultipleField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add multiple-user custom fields', options: [ { name: 'userMultipleFields', displayName: 'Multiple User Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getMultipleUserFields', }, default: '', description: 'Select the user field', }, { displayName: 'Action', name: 'action', type: 'options', options: [ { name: 'Replace', value: 'replace', description: 'Replace all existing values' }, { name: 'Add', value: 'add', description: 'Add a value to the existing list' }, { name: 'Remove', value: 'remove', description: 'Remove a value from the existing list', }, ], default: 'replace', description: 'Whether to replace, add to, or remove from the existing values', }, { displayName: 'Users', name: 'fieldValues', type: 'multiOptions', typeOptions: { loadOptionsMethod: 'getUsers', }, default: [], description: 'Select multiple users from the list', }, ], }, ], }, { displayName: 'Contact Field (Single)', name: 'contactSingleField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add single-contact custom fields', options: [ { name: 'contactSingleFields', displayName: 'Single Contact Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getSingleContactFields', }, default: '', description: 'Select the contact field', }, { displayName: 'Contact ID', name: 'fieldValue', type: 'string', default: '', description: 'Enter the contact ID', placeholder: 'contact_xxxxxxxxxxxxxxxx', }, ], }, ], }, { displayName: 'Contact Field (Multiple)', name: 'contactMultipleField', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Add multiple-contact custom fields', options: [ { name: 'contactMultipleFields', displayName: 'Multiple Contact Fields', values: [ { displayName: 'Field Name', name: 'fieldId', type: 'options', typeOptions: { loadOptionsMethod: 'getMultipleContactFields', }, default: '', description: 'Select the contact field', }, { displayName: 'Action', name: 'action', type: 'options', options: [ { name: 'Replace', value: 'replace', description: 'Replace all existing values' }, { name: 'Add', value: 'add', description: 'Add a value to the existing list' }, { name: 'Remove', value: 'remove', description: 'Remove a value from the existing list', }, ], default: 'replace', description: 'Whether to replace, add to, or remove from the existing values', }, { displayName: 'Contact IDs', name: 'fieldValues', type: 'string', default: '', description: 'Enter contact IDs separated by commas', placeholder: 'contact_xxx..., contact_yyy...', }, ], }, ], }, ], }, ]; /** * Custom Fields UI sections for Update operation (same structure as create) */ exports.customFieldsUpdateSections = exports.customFieldsCreateSections.map((section) => ({ ...section, displayOptions: { show: { resource: ['lead', 'opportunity'], operation: ['update'], }, }, })); /** * Load options methods for the new custom fields implementation */ exports.customFieldsLoadMethods = { /** * Get cached custom fields with workspace isolation */ async getCachedCustomFields(context) { var _a; const workspaceId = await getWorkspaceId(context); // Determine the resource type from the context const resource = ((_a = context.getNodeParameter) === null || _a === void 0 ? void 0 : _a.call(context, 'resource', 0)) || 'lead'; const cacheKey = `${workspaceId}_${resource}`; const cached = customFieldsCache.get(cacheKey); if (cached && isCacheValid(cached.timestamp, FIELD_CACHE_TTL)) { return cached.fields; } try { // Use the appropriate endpoint based on resource type const endpoint = resource === 'opportunity' ? '/custom_field/opportunity/' : '/custom_field/lead/'; // Prefer the all-items helper to flatten pagination and match JSON shapes let fieldsData = []; try { fieldsData = await GenericFunctions_1.closeApiRequestAllItems.call(context, 'data', 'GET', endpoint); } catch { // Fallback to single request and normalization const raw = await GenericFunctions_1.closeApiRequest.call(context, 'GET', endpoint); fieldsData = normalizeCustomFieldsResponse(raw); } if (!Array.isArray(fieldsData)) { console.error('Unexpected custom fields response format:', fieldsData); return []; } // Cache the results with resource-specific key customFieldsCache.set(cacheKey, { timestamp: Date.now(), fields: fieldsData, }); return fieldsData; } catch (error) { console.error('Error fetching custom fields:', error); return []; } }, /** * Get cached users with workspace isolation */ async getCachedUsers(context) { const workspaceId = await getWorkspaceId(context); const cached = usersCache.get(workspaceId); if (cached && isCacheValid(cached.timestamp, USER_CACHE_TTL)) { return cached.users; } try { // Use the existing Close API request function for consistency const users = await GenericFunctions_1.closeApiRequest.call(context, 'GET', '/user/'); const usersData = users && Array.isArray(users.data) ? users.data : normalizeCustomFieldsResponse(users); const userOptions = usersData.map((user) => ({ name: `${user.first_name} ${user.last_name} (${user.email})`, value: user.id, })); // Cache the results usersCache.set(workspaceId, { timestamp: Date.now(), users: userOptions, }); return userOptions; } catch (error) { console.error('Error fetching users:', error); return []; } }, /** * Get text fields */ async getTextFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'text') .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get number fields */ async getNumberFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'number') .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get date fields (includes both date and datetime) */ async getDateFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'date' || field.type === 'datetime') .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get single choice fields */ async getChoiceSingleFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'choices' && !field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get multiple choice fields */ async getChoiceMultipleFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'choices' && field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get single user fields */ async getUserSingleFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'user' && !field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get multiple user fields */ async getUserMultipleFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'user' && field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get single contact fields */ async getContactSingleFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'contact' && !field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get multiple contact fields */ async getContactMultipleFields(context) { const fields = await this.getCachedCustomFields(context); return fields .filter((field) => field.type === 'contact' && field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get all choice values from all choice fields * This is used as a workaround since loadOptionsDependsOn doesn't work in fixedCollections * It tries to filter based on the selected fieldId if possible */ async getAllChoiceValues(context) { var _a, _b, _c, _d, _e; try { // Try to get the selected fieldId from various sources let selectedFieldId; try { // First try to get the fieldId using getCurrentNodeParameter (works for the current editing field) try { selectedFieldId = context.getCurrentNodeParameter('fieldId'); } catch { // getCurrentNodeParameter might fail, try alternative approaches } // If not found, try to get from the path context if (!selectedFieldId) { // Try to get from current node parameters const customFields = (_a = context.getNodeParameter) === null || _a === void 0 ? void 0 : _a.call(context, 'customFields'); // Check single choice fields if ((_b = customFields === null || customFields === void 0 ? void 0 : customFields.choiceSingleField) === null || _b === void 0 ? void 0 : _b.choiceSingleFields) { const singleFields = customFields.choiceSingleField.choiceSingleFields; if (Array.isArray(singleFields)) { // Find the field that's currently being edited (the one without a value set) const currentField = singleFields.find((f) => f.fieldId && !f.fieldValue); if (currentField === null || currentField === void 0 ? void 0 : currentField.fieldId) { selectedFieldId = currentField.fieldId; } else if (singleFields.length > 0) { // Fallback to the last field selectedFieldId = (_c = singleFields[singleFields.length - 1]) === null || _c === void 0 ? void 0 : _c.fieldId; } } } // Check multiple choice fields if not found if (!selectedFieldId && ((_d = customFields === null || customFields === void 0 ? void 0 : customFields.choiceMultipleField) === null || _d === void 0 ? void 0 : _d.choiceMultipleFields)) { const multipleFields = customFields.choiceMultipleField.choiceMultipleFields; if (Array.isArray(multipleFields)) { // Find the field that's currently being edited (the one without values set) const currentField = multipleFields.find((f) => f.fieldId && (!f.fieldValues || f.fieldValues.length === 0)); if (currentField === null || currentField === void 0 ? void 0 : currentField.fieldId) { selectedFieldId = currentField.fieldId; } else if (multipleFields.length > 0) { // Fallback to the last field selectedFieldId = (_e = multipleFields[multipleFields.length - 1]) === null || _e === void 0 ? void 0 : _e.fieldId; } } } } } catch { // Silently fail and show all choices } const fields = await this.getCachedCustomFields(context); const choiceFields = fields.filter((f) => f.type === 'choices' && f.choices && Array.isArray(f.choices)); // If we have a selected field, only return its choices if (selectedFieldId) { const selectedField = choiceFields.find((f) => f.id === selectedFieldId); if (selectedField && selectedField.choices) { const result = selectedField.choices.map((choice) => ({ name: choice, value: choice, })); result.sort((a, b) => a.name.localeCompare(b.name)); return result; } } // Fallback: return all choices from all fields with field name label const allChoices = new Map(); for (const field of choiceFields) { if (field.choices) { for (const choice of field.choices) { const key = `${choice}__${field.id}`; allChoices.set(key, `${choice} (from ${field.name})`); } } } const result = Array.from(allChoices.entries()).map(([key, name]) => ({ name, value: key.split('__')[0], // Extract the actual choice value })); // Sort alphabetically by display name result.sort((a, b) => a.name.localeCompare(b.name)); return result; } catch (error) { console.error('[getAllChoiceValues] Error:', error); return []; } }, /** * Get choices/options for a specific field (handles choice fields) * NOTE: This method doesn't work in fixedCollections due to n8n limitations */ async getFieldChoices(context) { try { // Get the fieldId - n8n passes this when loadOptionsDependsOn is used let fieldId; // Try multiple ways to get the fieldId const attempts = [ { name: 'Direct fieldId', fn: () => context.getCurrentNodeParameter('fieldId') }, { name: 'Single choice path', fn: () => context.getCurrentNodeParameter('customFields.choiceSingleField.choiceSingleFields.fieldId'), }, { name: 'Multiple choice path', fn: () => context.getCurrentNodeParameter('customFields.choiceMultipleField.choiceMultipleFields.fieldId'), }, { name: 'Node parameters', fn: () => { var _a, _b, _c, _d, _e, _f; const params = context.getNodeParameter('customFields'); console.log('[getFieldChoices] Full customFields params:', JSON.stringify(params, null, 2)); if ((_c = (_b = (_a = params === null || params === void 0 ? void 0 : params.choiceSingleField) === null || _a === void 0 ? void 0 : _a.choiceSingleFields) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.fieldId) { return params.choiceSingleField.choiceSingleFields[0].fieldId; } if ((_f = (_e = (_d = params === null || params === void 0 ? void 0 : params.choiceMultipleField) === null || _d === void 0 ? void 0 : _d.choiceMultipleFields) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.fieldId) { return params.choiceMultipleField.choiceMultipleFields[0].fieldId; } return undefined; }, }, ]; for (const attempt of attempts) { try { const result = attempt.fn(); if (result) { console.log(`[getFieldChoices] Found fieldId via "${attempt.name}":`, result); fieldId = result; break; } } catch (e) { console.log(`[getFieldChoices] "${attempt.name}" failed:`, e instanceof Error ? e.message : e); } } // Log for debugging console.log('[getFieldChoices] Final fieldId:', fieldId); if (!fieldId) { console.log('[getFieldChoices] No fieldId found - returning empty'); return []; } // Fetch all custom fields const fields = await this.getCachedCustomFields(context); console.log(`[getFieldChoices] Found ${fields.length} total custom fields`); // Find the specific field const field = fields.find((f) => f.id === fieldId); if (!field) { console.log(`[getFieldChoices] Field not found for ID: ${fieldId}`); return []; } console.log(`[getFieldChoices] Found field: ${field.name}, type: ${field.type}`); // Handle choice fields if (field.type === 'choices') { if (!field.choices || !Array.isArray(field.choices) || field.choices.length === 0) { console.log(`[getFieldChoices] Field "${field.name}" has no choices`); return []; } console.log(`[getFieldChoices] Returning ${field.choices.length} choices for ${field.name}:`, field.choices); return field.choices.map((choice) => ({ name: choice, value: choice, })); } console.log(`[getFieldChoices] Field "${field.name}" is type "${field.type}", not "choices"`); return []; } catch (error) { console.error('[getFieldChoices] Error:', error); return []; } }, /** * Get fields filtered by the selected field type (legacy method) */ async getFieldsByType(context) { const fieldType = context.getCurrentNodeParameter('fieldType'); if (!fieldType) { return []; } const fields = await this.getCachedCustomFields(context); // Map field types to API types and cardinality const typeMapping = { choice_single: { type: 'choices', multiple: false }, choice_multiple: { type: 'choices', multiple: true }, text: { type: 'text' }, number: { type: 'number' }, date: { type: 'date' }, datetime: { type: 'datetime' }, user_single: { type: 'user', multiple: false }, user_multiple: { type: 'user', multiple: true }, contact_single: { type: 'contact', multiple: false }, contact_multiple: { type: 'contact', multiple: true }, }; const config = typeMapping[fieldType]; if (!config) { return []; } return fields .filter((field) => { if (field.type !== config.type) return false; return !(config.multiple !== undefined && field.accepts_multiple_values !== config.multiple); }) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Filter fields by type and cardinality (legacy method) */ filterFieldsByType(fields, type, multiple) { return fields .filter((field) => { if (field.type !== type) return false; return !(multiple !== undefined && field.accepts_multiple_values !== multiple); }) .map((field) => ({ name: `${field.name} (${field.accepts_multiple_values ? 'Multiple' : 'Single'})`, value: field.id, })); }, /** * Get choices for a specific field (legacy method) */ getChoicesForField(fields, fieldId) { const field = fields.find((f) => f.id === fieldId); if (!field || field.type !== 'choices' || !field.choices) { return []; } return field.choices.map((choice) => ({ name: choice, value: choice, })); }, /** * Expose users as loadOptions method for UI dropdowns */ async getUsers(context) { return this.getCachedUsers(context); }, /** * Get cached contact custom fields with workspace isolation */ async getCachedContactCustomFields(context) { const workspaceId = getWorkspaceId(context); const cacheKey = `${workspaceId}_contact`; const cached = customFieldsCache.get(cacheKey); if (cached && isCacheValid(cached.timestamp, FIELD_CACHE_TTL)) { return cached.fields; } try { // Use the contact custom fields endpoint const endpoint = '/custom_field/contact/'; // Prefer the all-items helper to flatten pagination and match JSON shapes let fieldsData = []; try { fieldsData = await GenericFunctions_1.closeApiRequestAllItems.call(context, 'data', 'GET', endpoint); } catch { // Fallback to single request and normalization const raw = await GenericFunctions_1.closeApiRequest.call(context, 'GET', endpoint); fieldsData = normalizeCustomFieldsResponse(raw); } if (!Array.isArray(fieldsData)) { console.error('Unexpected custom fields response format:', fieldsData); return []; } // Cache the results with resource-specific key customFieldsCache.set(cacheKey, { timestamp: Date.now(), fields: fieldsData, }); return fieldsData; } catch (error) { console.error('Error fetching contact custom fields:', error); return []; } }, /** * Get contact text fields */ async getContactTextFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'text') .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get contact number fields */ async getContactNumberFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'number') .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get contact date fields (includes both date and datetime) */ async getContactDateFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'date' || field.type === 'datetime') .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get contact single choice fields */ async getContactSingleChoiceFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'choices' && !field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get contact multiple choice fields */ async getContactMultipleChoiceFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'choices' && field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get all contact choice values from all choice fields */ async getContactAllChoiceValues(context) { try { const fields = await this.getCachedContactCustomFields(context); const choiceFields = fields.filter((f) => f.type === 'choices' && f.choices && Array.isArray(f.choices)); // Return all choices from all fields with field name label const allChoices = new Map(); for (const field of choiceFields) { if (field.choices) { for (const choice of field.choices) { const key = `${choice}__${field.id}`; allChoices.set(key, `${choice} (from ${field.name})`); } } } const result = Array.from(allChoices.entries()).map(([key, name]) => ({ name, value: key.split('__')[0], // Extract the actual choice value })); // Sort alphabetically by display name result.sort((a, b) => a.name.localeCompare(b.name)); return result; } catch (error) { console.error('[getContactAllChoiceValues] Error:', error); return []; } }, /** * Get contact single user fields */ async getContactSingleUserFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'user' && !field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, /** * Get contact multiple user fields */ async getContactMultipleUserFields(context) { const fields = await this.getCachedContactCustomFields(context); return fields .filter((field) => field.type === 'user' && field.accepts_multiple_values) .map((field) => ({ name: field.name, value: field.id, })); }, }; /** * Validation functions for custom field values */ exports.customFieldValidators = { /** * Validate text field value */ validateText(value) { if (typeof value !== 'string') { return 'Text field value must be a string'; } if (value.length > 1000) { return 'Text field value cannot exceed 1000 characters'; } return null; }, /** * Validate number field value */ validateNumber(value) { const num = Number(value); if (isNaN(num)) { return 'Number field value must be a valid number'; } return null; }, /** * Validate date field value */ validateDate(value) { if (typeof value !== 'string') { return 'Date field value must be a string in ISO format'; } const date = new Date(value); if (isNaN(date.getTime())) { return 'Date field value must be a valid date'; } return null; }, /** * Validate choice field value */ validateChoice(value, field) { if (!field.choices) { return 'Field has no available choices'; } if (field.accepts_multiple_values) { if (!Array.isArray(value)) { return 'Multiple choice field value must be an array'; } for (const v of value) { if (!field.choices.includes(v)) { return `Invalid choice: ${v}`; } } } else { if (!field.choices.includes(value)) { return `Invalid choice: ${value}`; } } return null; }, /** * Validate user field value */ validateUser(value, field) { const values = field.accepts_multiple_values ? Array.isArray(value) ? value : [value] : [value]; for (const v of values) { const s = String(v); if (s.indexOf('user_') !== 0) { return `Invalid user ID format: ${v}`; } } return null; }, /** * Validate contact field value */ validateContact(value, field) { const values = field.accepts_multiple_values ? Array.isArray(value) ? value : [value] : [value]; for (const v of values) { const s = String(v); if (s.indexOf('cont_') !== 0) { return `Invalid contact ID format: ${v}`; } } return null; }, }; /** * Utility function to construct custom field payload */ function constructCustomFieldsPayload(customFieldsData, fields) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const payload = {}; // Handle the new collection-based custom fields structure const customFields = customFieldsData.customFields; if (!customFields) { return payload; } // Helper function to process field values const processFields = (fieldsArray, fieldType) => { if (!Array.isArray(fieldsArray)) return; for (const fieldData of fieldsArray) { const { fieldId, fieldValue, fieldValues, action } = fieldData; if (!fieldId) { continue; } const field = fields.find((f) => f.id === fieldId); if (!field) { continue; } let value; // Determine the value based on field type switch (fieldType) { case 'text': case 'date': case 'choiceSingle': case 'userSingle': case 'contactSingle': value = fieldValue; break; case 'number': value = Number(fieldValue); if (isNaN(value)) { throw new Error(`Custom field "${field.name}" validation error: Number field value must be a valid number`); } break; case 'choiceMultiple': case 'userMultiple': value = fieldValues; break; case 'contactMultiple': // Handle comma-separated string to array conversion if (typeof fieldValues === 'string') { value = String(fieldValues) .split(',') .map((id) => id.trim()) .filter((id) => id); } else if (Array.isArray(fieldValues)) {