UNPKG

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
"use strict"; 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;