n8n-nodes-mautic-advanced
Version:
Enhanced n8n node for Mautic with comprehensive API coverage including tags, campaigns, categories, and advanced contact management
362 lines (361 loc) • 14.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MauticAdvancedAiTools = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const tool_executor_1 = require("./ai-tools/tool-executor");
const description_builders_1 = require("./ai-tools/description-builders");
const schema_generator_1 = require("./ai-tools/schema-generator");
const runtime_1 = require("./ai-tools/runtime");
const error_formatter_1 = require("./ai-tools/error-formatter");
const runtimeSchemas = (0, schema_generator_1.getRuntimeSchemaBuilders)(runtime_1.runtimeZod);
const OPERATION_LABELS = {
get: 'Get by ID',
getAll: 'Get many (with filters)',
create: 'Create',
update: 'Update',
delete: 'Delete',
sendEmail: 'Send email to contact',
addToSegments: 'Add to segments',
removeFromSegments: 'Remove from segments',
addToCampaigns: 'Add to campaigns',
removeFromCampaigns: 'Remove from campaigns',
addContact: 'Add contact',
removeContact: 'Remove contact',
add: 'Add association',
remove: 'Remove association',
send: 'Send segment email',
};
const WRITE_OPERATIONS = [
'create',
'update',
'delete',
'sendEmail',
'addToSegments',
'removeFromSegments',
'addToCampaigns',
'removeFromCampaigns',
'addContact',
'removeContact',
'add',
'remove',
'send',
];
const RESOURCE_OPERATIONS = {
contact: {
label: 'Contact',
ops: [
'get',
'getAll',
'create',
'update',
'delete',
'sendEmail',
'addToSegments',
'removeFromSegments',
'addToCampaigns',
'removeFromCampaigns',
],
},
company: {
label: 'Company',
ops: ['get', 'getAll', 'create', 'update', 'delete'],
},
campaign: {
label: 'Campaign',
ops: ['get', 'getAll', 'create', 'update', 'delete'],
},
email: {
label: 'Email',
ops: ['get', 'getAll', 'create', 'update', 'delete'],
},
segment: {
label: 'Segment',
ops: ['get', 'getAll', 'create', 'update', 'delete', 'addContact', 'removeContact'],
},
tag: {
label: 'Tag',
ops: ['get', 'getAll', 'create', 'update', 'delete'],
},
note: {
label: 'Note',
ops: ['get', 'getAll', 'create', 'update', 'delete'],
},
category: {
label: 'Category',
ops: ['get', 'getAll', 'create', 'update', 'delete'],
},
field: {
label: 'Field',
ops: ['get', 'getAll'],
},
user: {
label: 'User',
ops: ['get', 'getAll'],
},
companyContact: {
label: 'Company Contact',
ops: ['add', 'remove'],
},
campaignContact: {
label: 'Campaign Contact',
ops: ['add', 'remove'],
},
contactSegment: {
label: 'Contact Segment',
ops: ['add', 'remove'],
},
segmentEmail: {
label: 'Segment Email',
ops: ['send'],
},
};
const EXECUTE_METADATA_FIELDS = new Set([
'resource',
'operation',
'tool',
'toolName',
'toolCallId',
'sessionId',
'action',
'chatInput',
// Named rule: "root field injection" — n8n canvas UUID
'root',
]);
function getDefaultOperation(operations) {
if (operations.includes('getAll'))
return 'getAll';
if (operations.includes('get'))
return 'get';
return operations[0] ?? '';
}
function parseToolResult(resultJson) {
try {
return JSON.parse(resultJson);
}
catch {
return { error: resultJson };
}
}
function stripExecuteMetadata(params) {
const cleaned = {};
for (const [key, value] of Object.entries(params)) {
if (!EXECUTE_METADATA_FIELDS.has(key))
cleaned[key] = value;
}
return cleaned;
}
class MauticAdvancedAiTools {
constructor() {
this.description = {
displayName: 'Mautic Advanced AI Tools',
name: 'mauticAdvancedAiTools',
icon: 'file:MauticAdvancedIcon.svg',
group: ['output'],
version: 1,
description: 'Expose Mautic Advanced operations as AI tools for the AI Agent',
defaults: { name: 'Mautic Advanced AI Tools' },
inputs: [],
outputs: [{ type: 'ai_tool', displayName: 'Tools' }],
credentials: [
{
name: 'mauticAdvancedApi',
required: true,
displayOptions: {
show: {
authentication: ['credentials'],
},
},
},
{
name: 'mauticAdvancedOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Credentials',
value: 'credentials',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'credentials',
},
{
displayName: 'Resource Name or ID',
name: 'resource',
type: 'options',
required: true,
noDataExpression: true,
typeOptions: { loadOptionsMethod: 'getToolResources' },
default: '',
description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
},
{
displayName: 'Operations Names or IDs',
name: 'operations',
type: 'multiOptions',
required: true,
typeOptions: {
loadOptionsMethod: 'getToolResourceOperations',
loadOptionsDependsOn: ['resource', 'allowWriteOperations'],
},
default: [],
description: 'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
},
{
displayName: 'Allow Write Operations',
name: 'allowWriteOperations',
type: 'boolean',
default: false,
description: 'Whether to enable mutating tools (create, update, delete, send, etc). Disabled = read-only.',
},
],
};
this.methods = {
loadOptions: {
async getToolResources() {
return Object.entries(RESOURCE_OPERATIONS)
.map(([value, config]) => ({
name: config.label,
value,
description: `${config.label} resource`,
}))
.sort((a, b) => a.name.localeCompare(b.name));
},
async getToolResourceOperations() {
const resource = this.getCurrentNodeParameter('resource');
const allowWrite = (this.getCurrentNodeParameter('allowWriteOperations') ??
false);
if (!resource)
return [];
const config = RESOURCE_OPERATIONS[resource];
if (!config)
return [];
return config.ops
.filter((op) => allowWrite || !WRITE_OPERATIONS.includes(op))
.map((op) => ({
name: OPERATION_LABELS[op] ?? op,
value: op,
description: `${op} operation for ${config.label}`,
}));
},
},
};
}
async supplyData(itemIndex) {
const resource = this.getNodeParameter('resource', itemIndex);
const operations = this.getNodeParameter('operations', itemIndex);
const allowWriteOperations = this.getNodeParameter('allowWriteOperations', itemIndex, false);
if (!resource)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Resource is required');
if (!operations?.length)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'At least one operation must be selected');
const config = RESOURCE_OPERATIONS[resource];
if (!config)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown resource: ${resource}`);
// Layer 1 write safety — filter operations based on allowWriteOperations toggle
const enabledOperations = operations.filter((op) => {
if (WRITE_OPERATIONS.includes(op) && !allowWriteOperations)
return false;
return config.ops.includes(op);
});
if (enabledOperations.length === 0) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No tools to expose. Select operations and enable "Allow Write Operations" if needed.');
}
// Detect if resource supports search by checking the getAll schema
const getAllSchema = runtimeSchemas.buildUnifiedSchema(resource, ['getAll']);
const supportsSearch = 'search' in getAllSchema.shape;
const referenceUtc = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
const unifiedSchema = runtimeSchemas.buildUnifiedSchema(resource, enabledOperations);
const unifiedDescription = (0, description_builders_1.buildUnifiedDescription)(config.label, resource, enabledOperations, referenceUtc, supportsSearch);
// Tool name must match ^[a-zA-Z0-9_-]{1,128}$ — no spaces, ASCII only, unique per node
const toolName = `mauticadvanced_${resource}`;
const unifiedTool = new runtime_1.RuntimeDynamicStructuredTool({
name: toolName,
description: unifiedDescription,
schema: unifiedSchema,
func: async (params) => {
const operationFromArgs = params.operation;
const operation = typeof operationFromArgs === 'string' ? operationFromArgs : undefined;
// Layer 2 write safety — re-check after schema parsing (defense-in-depth)
if (operation && WRITE_OPERATIONS.includes(operation) && !allowWriteOperations) {
return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.WRITE_OPERATION_BLOCKED, 'Write operations are disabled for this tool.', 'Enable allowWriteOperations on the MauticAdvancedAiTools node to use mutating operations.'));
}
if (!operation || !enabledOperations.includes(operation)) {
return JSON.stringify((0, error_formatter_1.wrapError)(resource, operationFromArgs ?? 'unknown', error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Missing or unsupported operation for this tool call.', `Allowed operations: ${enabledOperations.join(', ')}.`));
}
const operationParams = { ...params };
delete operationParams.operation;
return (0, tool_executor_1.executeAiTool)(this, resource, operation, operationParams, enabledOperations);
},
});
return { response: unifiedTool };
}
async execute() {
const resource = this.getNodeParameter('resource', 0);
const operations = this.getNodeParameter('operations', 0);
const allowWriteOperations = this.getNodeParameter('allowWriteOperations', 0, false);
if (!resource || !operations?.length) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Resource and at least one operation must be configured.');
}
const config = RESOURCE_OPERATIONS[resource];
if (!config)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown resource: ${resource}`);
const effectiveOps = operations.filter((op) => !WRITE_OPERATIONS.includes(op) || allowWriteOperations);
if (effectiveOps.length === 0) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No permitted operations. Enable "Allow Write Operations" if needed.');
}
const items = this.getInputData();
const response = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const item = items[itemIndex];
if (!item)
continue;
const requestedOp = item.json.operation;
// Layer 3 write safety — execute() path
if (requestedOp && WRITE_OPERATIONS.includes(requestedOp) && !allowWriteOperations) {
response.push({
json: parseToolResult(JSON.stringify((0, error_formatter_1.wrapError)(resource, requestedOp, error_formatter_1.ERROR_TYPES.WRITE_OPERATION_BLOCKED, 'Write operations are disabled.', 'Enable allowWriteOperations on this node to use mutating operations.'))),
pairedItem: { item: itemIndex },
});
continue;
}
const effectiveOp = requestedOp && effectiveOps.includes(requestedOp)
? requestedOp
: getDefaultOperation(effectiveOps);
try {
const params = stripExecuteMetadata(item.json);
const resultJson = await (0, tool_executor_1.executeAiTool)(this, resource, effectiveOp, params, effectiveOps);
response.push({
json: parseToolResult(resultJson),
pairedItem: { item: itemIndex },
});
}
catch (error) {
if (this.continueOnFail()) {
response.push({
json: { error: error instanceof Error ? error.message : String(error) },
pairedItem: { item: itemIndex },
});
continue;
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error instanceof Error ? error.message : String(error), { itemIndex });
}
}
return [response];
}
}
exports.MauticAdvancedAiTools = MauticAdvancedAiTools;