UNPKG

n8n-nodes-inflow-crm

Version:

Official Inflow CRM integration for n8n. Create, update, search records and handle webhooks with dynamic field support.

431 lines (430 loc) 21.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.mapTriggerTypeToEvent = exports.processFileUploads = exports.getInflowFieldsForResourceMapper = exports.getProcessModules = exports.getModules = void 0; const api_1 = require("./api"); const types_1 = require("./types"); const metadataCache_1 = require("./metadataCache"); const logger_1 = require("./logger"); const fieldsAccess_1 = require("../access/fieldsAccess"); const endpoints_1 = require("./endpoints"); async function getModules() { try { logger_1.apiLogger.info('Fetching bundle modules from /api/getBundleModuleNames'); const response = await (0, api_1.inflowApiRequest)(this, 'GET', endpoints_1.Endpoints.GET_BUNDLE_MODULE_NAMES); if (!response || typeof response !== 'object' || !('data' in response) || !Array.isArray(response.data)) { logger_1.apiLogger.warn('Invalid response format:', response); return []; } const modules = response.data; logger_1.apiLogger.info(`Found ${modules.length} modules from API`); // Build translation map: moduleName -> plural.denominator const translationMap = new Map(); for (const mod of modules) { if (mod.moduleName && mod.singular && mod.plural && mod.plural.denominator) { translationMap.set(mod.moduleName, mod.plural.denominator); } else { logger_1.apiLogger.warn('Missing translation for module:', mod.moduleName); } } const filteredModules = modules .map(mod => mod.moduleName) .filter(moduleName => types_1.SUPPORTED_MODULES.includes(moduleName)); logger_1.apiLogger.info(`Filtered to ${filteredModules.length} supported modules:`, filteredModules); const moduleOptions = filteredModules.map(moduleName => ({ name: translationMap.get(moduleName) ?? moduleName, value: moduleName, })); logger_1.apiLogger.info(`Returning ${moduleOptions.length} options`); return moduleOptions; } catch (error) { logger_1.apiLogger.error('Failed to load Inflow CRM modules:', error); logger_1.apiLogger.error('Error stack:', error.stack); throw error; } } exports.getModules = getModules; /** * Returns only modules that support processes (potential and order). * Used by the getProcesses operation. */ async function getProcessModules() { try { logger_1.apiLogger.info('Fetching process-enabled modules (potential and order)'); const response = await (0, api_1.inflowApiRequest)(this, 'GET', endpoints_1.Endpoints.GET_BUNDLE_MODULE_NAMES); if (!response || typeof response !== 'object' || !('data' in response) || !Array.isArray(response.data)) { logger_1.apiLogger.warn('Invalid response format:', response); return []; } const modules = response.data; logger_1.apiLogger.info(`Found ${modules.length} modules from API`); // Build translation map: moduleName -> plural.denominator const translationMap = new Map(); for (const mod of modules) { if (mod.moduleName && mod.singular && mod.plural && mod.plural.denominator) { translationMap.set(mod.moduleName, mod.plural.denominator); } else { logger_1.apiLogger.warn('Missing translation for module:', mod.moduleName); } } // Only include potential and order modules (modules that support processes) const processModules = ['potential', 'order']; const filteredModules = modules .map(mod => mod.moduleName) .filter(moduleName => processModules.includes(moduleName)); logger_1.apiLogger.info(`Filtered to ${filteredModules.length} process-enabled modules:`, filteredModules); const moduleOptions = filteredModules.map(moduleName => ({ name: translationMap.get(moduleName) ?? moduleName, value: moduleName, })); logger_1.apiLogger.info(`Returning ${moduleOptions.length} process module options`); return moduleOptions; } catch (error) { logger_1.apiLogger.error('Failed to load process modules:', error); logger_1.apiLogger.error('Error stack:', error.stack); throw error; } } exports.getProcessModules = getProcessModules; /** * Maps Inflow CRM field types to n8n resource mapper field types. * This determines what kind of input UI is shown for a field. */ function mapInflowTypeToResourceMapperType(inflowType) { logger_1.apiLogger.info(`Mapping type: ${inflowType}`); switch (inflowType) { case 'String': case 'Email': case 'Phone': case 'Link': case 'Nip': case 'File': return 'string'; case 'Number': case 'Rating': return 'number'; case 'Date': case 'Datetime': case 'DateRange': return 'dateTime'; case 'CheckBox': return 'boolean'; case 'PickList': case 'RadioButton': return 'options'; case 'PotentialStage': case 'Relation': return 'string'; default: logger_1.apiLogger.warn(`Unknown type: ${inflowType}, defaulting to string`); return 'string'; } } /** * The core function for the Resource Mapper. It fetches fields from the Inflow API * and transforms them into a structure that the n8n UI understands. */ async function getInflowFieldsForResourceMapper() { logger_1.apiLogger.info('===== GETTING INFLOW FIELDS FOR RESOURCE MAPPER ====='); try { const moduleName = this.getCurrentNodeParameter('module'); const operation = this.getNodeParameter('operation'); logger_1.apiLogger.info(`Module: "${moduleName}", Operation: "${operation}"`); if (!moduleName) { logger_1.apiLogger.warn('No module name provided, returning empty fields'); return { fields: [] }; } logger_1.apiLogger.info(`Getting fields for module: "${moduleName}", operation: "${operation}"`); const response = await (0, api_1.inflowApiRequest)(this, 'POST', endpoints_1.Endpoints.GET_MODULE_FIELDS, { module: moduleName }); logger_1.apiLogger.info('API Response:', JSON.stringify(response, null, 2)); // Handle Public API v1 response format: { data: [...] } let fields; if (response && typeof response === 'object' && 'data' in response) { fields = response.data; } else if (Array.isArray(response)) { // Fallback for old API format (direct array) fields = response; } else { logger_1.apiLogger.warn('Unexpected response format:', response); return { fields: [] }; } if (!fields || !Array.isArray(fields)) { logger_1.apiLogger.warn('No valid fields array found'); return { fields: [] }; } logger_1.apiLogger.info(`Processing ${fields.length} fields`); (0, metadataCache_1.cacheModuleFields)(moduleName, fields); const resourceMapperFields = await Promise.all(fields .filter(field => { const hasApiFieldName = !!field.apiFieldName; const isRequired = operation === 'create' && field.required; // Zawsze wyświetlaj pola które mają apiFieldName lub są wymagane if (!hasApiFieldName && !isRequired) { logger_1.apiLogger.info('Skipping field without apiFieldName and not required:', field); return false; } if (!hasApiFieldName && isRequired) { logger_1.apiLogger.warn('Required field without apiFieldName - will be displayed but may not work properly:', field); } return true; }) .map(async (field, index) => { logger_1.apiLogger.info(`Processing field ${index + 1}/${fields.length}: "${field.label || field.name}" (${field.type})`); const canBeUsedToMatch = operation === 'search' ? field.unique || ['id', 'name', 'email', 'nip'].includes(field.apiFieldName) : false; const n8nType = mapInflowTypeToResourceMapperType(field.type); const isRequired = operation === 'create' ? field.required || false : false; const fieldId = field.apiFieldName || field.name || `field_${index}`; const mapperField = { id: fieldId, displayName: field.label || field.name || fieldId, required: isRequired, display: true, type: n8nType, canBeUsedToMatch: canBeUsedToMatch, defaultMatch: operation === 'search' && field.apiFieldName === 'id', }; // Add options for picklist fields if (field.type === 'PickList' || field.type === 'RadioButton') { if (field.pickListValues && Array.isArray(field.pickListValues)) { mapperField.options = field.pickListValues.map(value => ({ name: typeof value === 'string' ? value : String(value), value: typeof value === 'string' ? value : String(value), })); logger_1.apiLogger.info(`Added ${field.pickListValues.length} options for field ${field.apiFieldName} , ${mapperField.type}`); } else { mapperField.options = []; } } // Add options for user relation fields directly in the loop if (field.type === 'Relation' && field.relatedModule === 'user') { logger_1.apiLogger.info(`Loading user options for relation field: ${field.apiFieldName}`); const userOptions = await fieldsAccess_1.loadUserOptions.call(this); mapperField.type = 'options'; if (userOptions.length > 0) { mapperField.options = userOptions; logger_1.apiLogger.info(`Added ${userOptions.length} user options for field ${field.apiFieldName}`); } else { logger_1.apiLogger.warn(`No user options loaded for field ${field.apiFieldName}`); } } // Add detection logic for process relation fields (type === 'Relation' && relatedModule === 'pipeline') if (field.type === 'Relation' && field.relatedModule === 'pipeline') { logger_1.apiLogger.info(`Processing process relation field: ${field.apiFieldName} for module: ${moduleName}`); // Integrate loadProcessOptions call for potential and order modules if (moduleName === 'potential' || moduleName === 'order') { try { const { loadProcessOptions } = await Promise.resolve().then(() => __importStar(require('../access/fieldsAccess'))); const processOptions = await loadProcessOptions.call(this); mapperField.type = 'options'; mapperField.options = processOptions; logger_1.apiLogger.info(`Added ${processOptions.length} process options for field ${field.apiFieldName}`); } catch (error) { logger_1.apiLogger.error(`Failed to load process options for field ${field.apiFieldName}:`, error); mapperField.type = 'string'; } } else { logger_1.apiLogger.info(`Module ${moduleName} does not support process relations, keeping as string field`); } } // Add special handling for stage fields in potential and order modules const isStageField = /stage/i.test(field.apiFieldName); if (isStageField && (moduleName === 'potential' || moduleName === 'order')) { logger_1.apiLogger.info(`Processing stage field "${field.apiFieldName}" for process-enabled module: ${moduleName}`); const hasProcessField = fields.some(f => f.type === 'Relation' && f.relatedModule === 'pipeline'); if (hasProcessField) { mapperField.required = false; mapperField.display = false; logger_1.apiLogger.info(`Stage field "${field.apiFieldName}" hidden for ${moduleName}: required=false, display=false (process field present)`); } else { logger_1.apiLogger.info(`Stage field "${field.apiFieldName}" kept as-is for ${moduleName}: no process field found`); } } logger_1.apiLogger.info('Created mapper field:', JSON.stringify(mapperField, null, 2)); return mapperField; })); logger_1.apiLogger.info(`Successfully transformed ${resourceMapperFields.length} fields for resource mapper`); logger_1.apiLogger.info('===== RESOURCE MAPPER COMPLETE ====='); // Ensure 'ID' field is present for 'findOne' operation if (operation === 'findOne') { const hasIdField = resourceMapperFields.some(f => f.id === 'id'); if (!hasIdField) { resourceMapperFields.unshift({ id: 'id', displayName: 'ID', required: true, display: true, type: 'string', canBeUsedToMatch: true, defaultMatch: true, }); logger_1.apiLogger.info('Injected ID field for findOne operation'); } } return { fields: resourceMapperFields }; } catch (error) { logger_1.apiLogger.error('Failed to load fields for resource mapper:', error); logger_1.apiLogger.error('Error stack:', error.stack); throw error; // Re-throw to see error in n8n UI } } exports.getInflowFieldsForResourceMapper = getInflowFieldsForResourceMapper; /** * Processes file uploads for create/update actions. * Supports both binary data from previous nodes and file paths. */ async function processFileUploads(itemIndex, fileConfigs) { logger_1.apiLogger.info(`Processing ${fileConfigs.length} file uploads for item ${itemIndex}`); const processedFiles = []; const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB // MIME type validation removed as per CRM compatibility requirements for (const config of fileConfigs) { try { if (config.fileSource === 'binaryData') { logger_1.apiLogger.info(`Processing binary data for field: ${config.fieldName}`); const binaryPropertyName = config.binaryPropertyName || 'data'; const items = this.getInputData(); const item = items[itemIndex]; if (!item.binary || !item.binary[binaryPropertyName]) { throw new Error(`Binary data '${binaryPropertyName}' not found for file field '${config.fieldName}'`); } const binaryData = item.binary[binaryPropertyName]; const fileData = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); // Ensure fileName is never null/undefined let fileName = binaryData.fileName; if (!fileName || fileName.trim() === '') { fileName = `upload_${config.fieldName}_${Date.now()}`; } // Ensure mimeType is never null/undefined const mimeType = binaryData.mimeType || 'application/octet-stream'; // File size validation if (fileData.length > MAX_FILE_SIZE) { throw new Error(`file size exceeds limit (${fileData.length} bytes) for field '${config.fieldName}'`); } // File type validation removed logger_1.apiLogger.info('File validation:', { originalFileName: binaryData.fileName, processedFileName: fileName, originalMimeType: binaryData.mimeType, processedMimeType: mimeType, }); processedFiles.push({ fieldName: config.fieldName, fileData, fileName, mimeType, }); logger_1.apiLogger.info(`Successfully processed binary file: ${fileName} for field: ${config.fieldName}`); } else if (config.fileSource === 'filePath' && config.filePath) { logger_1.apiLogger.info(`Processing file path for field: ${config.fieldName}: ${config.filePath}`); const fs = await Promise.resolve().then(() => __importStar(require('fs'))).then(m => m.promises); const path = await Promise.resolve().then(() => __importStar(require('path'))); try { await fs.access(config.filePath); } catch { throw new Error(`File not found: ${config.filePath} for field '${config.fieldName}'`); } const fileData = await fs.readFile(config.filePath); const fileName = path.basename(config.filePath); // File size validation if (fileData.length > MAX_FILE_SIZE) { throw new Error(`file size exceeds limit (${fileData.length} bytes) for field '${config.fieldName}'`); } // File type validation (best effort: guess from extension) const ext = fileName.split('.').pop()?.toLowerCase(); const guessedMime = ext === 'pdf' ? 'application/pdf' : ext === 'zip' ? 'application/zip' : ext === 'png' ? 'image/png' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'txt' ? 'text/plain' : ext === 'doc' ? 'application/msword' : ext === 'docx' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : ext === 'xls' ? 'application/vnd.ms-excel' : ext === 'xlsx' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'application/octet-stream'; // File type validation removed processedFiles.push({ fieldName: config.fieldName, fileData, fileName, mimeType: guessedMime, }); logger_1.apiLogger.info(`Successfully processed file: ${fileName} for field: ${config.fieldName}`); } else { throw new Error(`Invalid file configuration for field '${config.fieldName}'`); } } catch (error) { logger_1.apiLogger.error(`Error processing file for field '${config.fieldName}':`, error); throw error; } } logger_1.apiLogger.info(`Successfully processed ${processedFiles.length} files`); return processedFiles; } exports.processFileUploads = processFileUploads; // --- WEBHOOK TRIGGER UTILS --- /** * Map n8n UI triggerType ('created', 'updated', 'deleted') to API event ('create', 'update', 'delete') */ function mapTriggerTypeToEvent(triggerType) { if (typeof triggerType !== 'string' || !triggerType) { throw new Error('Trigger type is required'); } const normalized = triggerType.toLowerCase(); if (normalized === 'create' || normalized === 'created') return 'created'; if (normalized === 'update' || normalized === 'updated') return 'updated'; if (normalized === 'delete' || normalized === 'deleted') return 'deleted'; throw new Error(`Invalid trigger type: ${triggerType}`); } exports.mapTriggerTypeToEvent = mapTriggerTypeToEvent;