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
JavaScript
;
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;