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