UNPKG

n8n-nodes-mautic-advanced

Version:

Enhanced n8n node for Mautic with comprehensive API coverage including tags, campaigns, categories, and advanced contact management

242 lines (241 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.DEFAULT_MAUTIC_PAGE_SIZE = exports.mauticApiRequest = exports.getMauticV2Status = exports.getMauticVersion = void 0; const n8n_workflow_1 = require("n8n-workflow"); const authenticatedRequest_1 = require("./utils/authenticatedRequest"); const versionCache = new Map(); const VERSION_CACHE_TTL_MS = 5 * 60 * 1000; function normalizeInstanceUrl(raw) { const trimmed = raw.trim(); if (!trimmed) return ''; try { const u = new URL(trimmed); u.protocol = u.protocol.toLowerCase(); u.hostname = u.hostname.toLowerCase(); u.hash = ''; u.search = ''; u.pathname = u.pathname.replace(/\/+$/, ''); return u.toString().replace(/\/+$/, ''); } catch { return trimmed.replace(/\/+$/, ''); } } async function detectMauticV2(context) { const authMethod = context.getNodeParameter('authentication', 0, 'credentials'); const credentialType = authMethod === 'credentials' ? 'mauticAdvancedApi' : 'mauticAdvancedOAuth2Api'; const credentials = await context.getCredentials(credentialType); const baseUrl = normalizeInstanceUrl(credentials.url || ''); const cacheKey = `${credentialType}:${baseUrl}`; const cached = versionCache.get(cacheKey); if (cached && Date.now() < cached.expiresAt) { return { version: cached.version, v2Status: cached.v2Status }; } let v2Status; try { await mauticApiRequest.call(context, 'GET', '/v2/companies', {}, { page: 1 }, undefined, { Accept: 'application/json', }); v2Status = 'usable'; } catch (error) { const httpCode = String(error?.httpCode ?? ''); // 401/403 = v2 route exists but the credential is rejected/forbidden there. Mautic's v2 API // Platform firewall commonly rejects v1-style OAuth2 bearer tokens (Basic auth works). The // route exists, but this credential cannot use it — fall back to v1 for routing. // 404 / parse errors / network = v2 route absent → genuine v6 instance. v2Status = httpCode === '401' || httpCode === '403' ? 'unauthorized' : 'absent'; } // Routing version: only treat as v7 when v2 is actually usable, so that operations route to the // v1 endpoints (which work under this credential) whenever v2 is unreachable. Owner enrichment, // a v7-only feature, is gated separately on `v2Status === 'usable'`. const version = v2Status === 'usable' ? 'v7' : 'v6'; versionCache.set(cacheKey, { version, v2Status, expiresAt: Date.now() + VERSION_CACHE_TTL_MS }); return { version, v2Status }; } async function getMauticVersion(context) { return (await detectMauticV2(context)).version; } exports.getMauticVersion = getMauticVersion; /** * Returns whether the Mautic v2 (API Platform) endpoints are usable with the active credential. * Callers use this to decide whether v7-only enrichment is possible and to warn actionably when a * v2 route exists but the credential is rejected there (`unauthorized`). */ async function getMauticV2Status(context) { return (await detectMauticV2(context)).v2Status; } exports.getMauticV2Status = getMauticV2Status; async function mauticApiRequest(method, endpoint, body = {}, query, uri, headers) { const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials'); const options = { headers: headers || {}, method, qs: query, uri: uri || `/api${endpoint}`, json: true, }; if (['POST', 'PUT', 'PATCH'].includes(method)) { options.body = body; } try { const returnData = await (0, authenticatedRequest_1.requestMauticAuthenticated)(this, authenticationMethod, options); if (returnData?.errors) { // They seem to sometimes return 200 status but still error. // Preserve the full error object including details for better error handling throw new n8n_workflow_1.NodeApiError(this.getNode(), returnData, { httpCode: '400', description: returnData, }); } return returnData; } catch (error) { // Preserve error details when available for better error handling if (error instanceof n8n_workflow_1.NodeApiError || error instanceof n8n_workflow_1.NodeOperationError) { throw error; } throw new n8n_workflow_1.NodeApiError(this.getNode(), error); } } exports.mauticApiRequest = mauticApiRequest; /** * Make an API request to paginated mautic endpoint * and return all results */ exports.DEFAULT_MAUTIC_PAGE_SIZE = 100; function toPositiveInteger(value) { const numericValue = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : undefined; if (numericValue === undefined || !Number.isFinite(numericValue) || numericValue <= 0) { return undefined; } return Math.floor(numericValue); } function getPaginationStart(value) { const start = toPositiveInteger(value); return start ?? 0; } function getResponseItems(responseData, propertyName) { const responseProperty = responseData[propertyName]; if (!responseProperty || typeof responseProperty !== 'object') { return []; } if (Array.isArray(responseProperty)) { return responseProperty; } return Object.values(responseProperty); } async function mauticApiRequestAllItems(propertyName, method, endpoint, body = {}, query = {}, maxResults) { const returnData = []; const baseQuery = { ...query }; const requestedStart = getPaginationStart(baseQuery.start); const totalLimit = toPositiveInteger(maxResults); delete baseQuery.start; delete baseQuery.limit; let currentStart = requestedStart; while (totalLimit === undefined || returnData.length < totalLimit) { const remaining = totalLimit === undefined ? undefined : totalLimit - returnData.length; const pageLimit = remaining === undefined ? exports.DEFAULT_MAUTIC_PAGE_SIZE : Math.min(exports.DEFAULT_MAUTIC_PAGE_SIZE, remaining); const pageQuery = { ...baseQuery, limit: pageLimit, start: currentStart, }; try { const responseData = await mauticApiRequest.call(this, method, endpoint, body, pageQuery); if (responseData.errors) { throw new n8n_workflow_1.NodeApiError(this.getNode(), responseData); } const pageItems = getResponseItems(responseData, propertyName); if (!pageItems.length) { break; } returnData.push(...pageItems); if (pageItems.length < pageLimit) { break; } currentStart += pageItems.length; } catch (error) { if (error instanceof n8n_workflow_1.NodeApiError || error instanceof n8n_workflow_1.NodeOperationError) { throw error; } throw new n8n_workflow_1.NodeApiError(this.getNode(), error); } } return returnData; } exports.mauticApiRequestAllItems = mauticApiRequestAllItems; /** * Serialise the n8n fixedCollection 'where' structure into Mautic API query parameters. * Handles nested andX/orX logic recursively. * @param whereArray Array of conditions from the fixedCollection * @param prefix Used internally for recursion (should be omitted by callers) * @returns Object with keys/values for qs */ function serialiseMauticWhere(whereArray, prefix = 'where') { const params = {}; const dateFields = [ 'date_modified', 'date_added', 'last_active', 'date_identified', 'dateFrom', 'dateTo', ]; whereArray.forEach((condition, idx) => { const base = `${prefix}[${idx}]`; if (condition.expr === 'andX' || condition.expr === 'orX') { params[`${base}[expr]`] = condition.expr; // Nested conditions: recurse if (condition.nested && Array.isArray(condition.nested.conditions)) { // The value for 'val' is an array of nested conditions const nestedParams = serialiseMauticWhere(condition.nested.conditions, `${base}[val]`); Object.assign(params, nestedParams); } else { // Defensive: empty group params[`${base}[val]`] = []; } } else { // Simple condition if (condition.col) params[`${base}[col]`] = condition.col; if (condition.expr) params[`${base}[expr]`] = condition.expr; if (condition.val !== undefined && condition.val !== '') { let val = condition.val; // Auto-format date values for known date fields if (condition.col && dateFields.includes(condition.col) && typeof val === 'string' && (val.includes('T') || val.match(/^\d{4}-\d{2}-\d{2}/))) { // Try to parse and format as UTC 'YYYY-MM-DD HH:mm:ss' const d = new Date(val); if (!isNaN(d.getTime())) { const pad = (n) => n.toString().padStart(2, '0'); val = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`; } } params[`${base}[val]`] = val; } } }); return params; } exports.serialiseMauticWhere = serialiseMauticWhere; function validateJSON(json) { let result; try { result = JSON.parse(json); } catch (exception) { result = undefined; } return result; } exports.validateJSON = validateJSON;