UNPKG

n8n-nodes-mautic-advanced

Version:

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

688 lines (687 loc) 40.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.executeAiTool = void 0; const error_formatter_1 = require("./error-formatter"); const GenericFunctions_1 = require("../GenericFunctions"); const N8N_METADATA_FIELDS = new Set([ 'sessionId', 'action', 'chatInput', // Named rule: "root field injection" — n8n canvas UUID injected into every tool call 'root', 'tool', 'toolName', 'toolCallId', // Named rule: "operation leaks" — func() strips it but execute() path passes raw item.json 'operation', ]); const NUMERIC_FIELDS = new Set([ 'id', 'contactId', 'companyId', 'campaignId', 'segmentId', 'emailId', 'lead', 'limit', 'owner_id', ]); // Map resource names to their Mautic API response property keys const RESOURCE_PROPERTY_MAP = { contact: { single: 'contact', plural: 'contacts' }, company: { single: 'company', plural: 'companies' }, campaign: { single: 'campaign', plural: 'campaigns' }, email: { single: 'email', plural: 'emails' }, segment: { single: 'list', plural: 'lists' }, tag: { single: 'tag', plural: 'tags' }, note: { single: 'note', plural: 'notes' }, category: { single: 'category', plural: 'categories' }, field: { single: 'field', plural: 'fields' }, user: { single: 'user', plural: 'users' }, }; // Map resource names to their API endpoint paths const RESOURCE_ENDPOINT_MAP = { contact: '/contacts', company: '/companies', campaign: '/campaigns', email: '/emails', segment: '/segments', tag: '/tags', note: '/notes', category: '/categories', user: '/users', }; function isEmpty(result) { return (result === null || result === undefined || (Array.isArray(result) && result.length === 0) || (typeof result === 'object' && !Array.isArray(result) && Object.keys(result).length === 0)); } function buildFilters(params) { const filters = {}; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== '') { filters[key] = value; } } return filters; } async function apiRequest(context, method, endpoint, body = {}, query = {}) { return GenericFunctions_1.mauticApiRequest.call(context, method, endpoint, body, query); } async function getMauticVersionFromContext(context) { try { return await (0, GenericFunctions_1.getMauticVersion)(context); } catch { return 'v6'; } } async function executeAiTool(context, resource, operation, rawParams, validOps = []) { // Layer 3 write safety — defense-in-depth inside the executor itself const WRITE_OPS = new Set([ 'create', 'update', 'delete', 'sendEmail', 'addToSegments', 'removeFromSegments', 'addToCampaigns', 'removeFromCampaigns', 'addContact', 'removeContact', 'add', 'remove', 'send', ]); if (validOps.length > 0 && WRITE_OPS.has(operation) && !validOps.includes(operation)) { 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.')); } // Strip n8n framework metadata const params = {}; for (const [key, value] of Object.entries(rawParams)) { if (!N8N_METADATA_FIELDS.has(key)) params[key] = value; } // Coerce numeric strings to numbers (LLMs occasionally pass "10" instead of 10) for (const key of NUMERIC_FIELDS) { if (key in params && typeof params[key] === 'string' && /^\d+$/.test(params[key])) { params[key] = parseInt(params[key], 10); } } try { // Route to resource-specific handlers switch (resource) { case 'contact': return await executeContactTool(context, operation, params); case 'company': return await executeCrudTool(context, resource, operation, params); case 'campaign': return await executeCrudTool(context, resource, operation, params); case 'email': return await executeCrudTool(context, resource, operation, params); case 'segment': return await executeSegmentTool(context, operation, params); case 'tag': return await executeTagTool(context, operation, params); case 'note': return await executeCrudTool(context, resource, operation, params); case 'category': return await executeCrudTool(context, resource, operation, params); case 'field': return await executeFieldTool(context, operation, params); case 'user': return await executeReadOnlyTool(context, resource, operation, params); case 'companyContact': return await executeCompanyContactTool(context, operation, params); case 'campaignContact': return await executeCampaignContactTool(context, operation, params); case 'contactSegment': return await executeContactSegmentTool(context, operation, params); case 'segmentEmail': return await executeSegmentEmailTool(context, operation, params); default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unsupported resource.', 'Use one of the available resource tools.')); } } catch (error) { return JSON.stringify((0, error_formatter_1.formatApiError)(error, resource, operation)); } } exports.executeAiTool = executeAiTool; // --------------------------------------------------------------------------- // Standard CRUD handler (company, campaign, email, note, category) // --------------------------------------------------------------------------- async function executeCrudTool(context, resource, operation, params) { const propMap = RESOURCE_PROPERTY_MAP[resource]; const endpoint = RESOURCE_ENDPOINT_MAP[resource]; if (!propMap || !endpoint) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unknown resource.', '')); } switch (operation) { case 'get': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, `A numeric entity ID is required for ${resource}.get.`, `Call mauticadvanced_${resource} with operation 'getAll' and the 'search' parameter to find the record and get its numeric 'id' first.`)); const response = await apiRequest(context, 'GET', `${endpoint}/${params.id}`); const result = response[propMap.single]; if (isEmpty(result)) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.ENTITY_NOT_FOUND, `No ${resource} record found with ID ${params.id}.`, `Call mauticadvanced_${resource} with operation 'getAll' and the 'search' parameter to find the record by text, then use the numeric ID from the results.`)); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'getAll': { const { limit = 25, ...filterParams } = params; const effectiveLimit = limit; const qs = {}; if (filterParams.search) qs.search = filterParams.search; qs.limit = effectiveLimit; qs.orderBy = 'id'; qs.orderByDir = 'asc'; const response = await apiRequest(context, 'GET', endpoint, {}, qs); const records = response[propMap.plural] ? Object.values(response[propMap.plural]) : []; const filters = buildFilters(filterParams); const hasFilters = Object.keys(filters).length > 0; if (records.length === 0 && hasFilters) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.NO_RESULTS_FOUND, `No ${resource} records matched the provided filters.`, 'Broaden your search criteria, check for typos, or verify the record exists.', { filtersUsed: filters })); } const resultPayload = { items: records, count: records.length }; if (records.length >= effectiveLimit) { resultPayload.truncated = true; resultPayload.note = `Results capped at ${effectiveLimit}. Use filters to narrow or increase 'limit' (max 100).`; } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, resultPayload)); } case 'create': { const createBody = { ...params }; delete createBody.id; // Parse customFields JSON if present if (typeof createBody.customFields === 'string') { try { const custom = JSON.parse(createBody.customFields); delete createBody.customFields; Object.assign(createBody, custom); } catch { /* ignore parse errors */ } } // Handle comma-separated tags if (typeof createBody.tags === 'string') { createBody.tags = createBody.tags .split(',') .map((t) => t.trim()) .filter(Boolean); } const response = await apiRequest(context, 'POST', `${endpoint}/new`, createBody); const result = response[propMap.single]; return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'update': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, `A numeric entity ID is required for ${resource}.update.`, `Call mauticadvanced_${resource} with operation 'getAll' and the 'search' parameter to find the record and get its numeric 'id' first.`)); const { id, ...updateBody } = params; // Parse customFields JSON if present if (typeof updateBody.customFields === 'string') { try { const custom = JSON.parse(updateBody.customFields); delete updateBody.customFields; Object.assign(updateBody, custom); } catch { /* ignore parse errors */ } } // Handle comma-separated tags if (typeof updateBody.tags === 'string') { updateBody.tags = updateBody.tags .split(',') .map((t) => t.trim()) .filter(Boolean); } const response = await apiRequest(context, 'PATCH', `${endpoint}/${id}/edit`, updateBody); const result = response[propMap.single]; return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'delete': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, `A numeric entity ID is required for ${resource}.delete.`, `Call mauticadvanced_${resource} with operation 'getAll' and the 'search' parameter to find the record and get its numeric 'id' first.`)); await apiRequest(context, 'DELETE', `${endpoint}/${params.id}/delete`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, { id: params.id, deleted: true })); } default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unsupported operation.', 'Check allowed operations for this resource.')); } } // --------------------------------------------------------------------------- // Contact handler (extra operations: sendEmail, segments, campaigns) // --------------------------------------------------------------------------- async function executeContactTool(context, operation, params) { const resource = 'contact'; switch (operation) { case 'get': case 'getAll': case 'create': case 'update': case 'delete': return executeCrudTool(context, resource, operation, params); case 'sendEmail': { const contactId = params.id ?? params.contactId; const emailId = params.emailId; if (!contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' and 'search' to find the contact ID.")); if (!emailId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric email ID is required.', "Call mauticadvanced_email with operation 'getAll' and 'search' to find the email ID.")); const response = await apiRequest(context, 'POST', `/emails/${emailId}/contact/${contactId}/send`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); } case 'addToSegments': { const contactId = params.id; if (!contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' and 'search' to find the contact ID.")); const segmentIds = typeof params.segmentIds === 'string' ? params.segmentIds .split(',') .map((s) => s.trim()) .filter(Boolean) : Array.isArray(params.segmentIds) ? params.segmentIds : []; const results = []; for (const segmentId of segmentIds) { const resp = await apiRequest(context, 'POST', `/segments/${segmentId}/contact/${contactId}/add`); results.push(resp); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, results)); } case 'removeFromSegments': { const contactId = params.id; if (!contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' and 'search' to find the contact ID.")); const segmentIds = typeof params.segmentIds === 'string' ? params.segmentIds .split(',') .map((s) => s.trim()) .filter(Boolean) : Array.isArray(params.segmentIds) ? params.segmentIds : []; const results = []; for (const segmentId of segmentIds) { const resp = await apiRequest(context, 'POST', `/segments/${segmentId}/contact/${contactId}/remove`); results.push(resp); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, results)); } case 'addToCampaigns': { const contactId = params.id; if (!contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' and 'search' to find the contact ID.")); const campaignIds = typeof params.campaignIds === 'string' ? params.campaignIds .split(',') .map((s) => s.trim()) .filter(Boolean) : Array.isArray(params.campaignIds) ? params.campaignIds : []; const results = []; for (const campaignId of campaignIds) { const resp = await apiRequest(context, 'POST', `/campaigns/${campaignId}/contact/${contactId}/add`); results.push(resp); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, results)); } case 'removeFromCampaigns': { const contactId = params.id; if (!contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' and 'search' to find the contact ID.")); const campaignIds = typeof params.campaignIds === 'string' ? params.campaignIds .split(',') .map((s) => s.trim()) .filter(Boolean) : Array.isArray(params.campaignIds) ? params.campaignIds : []; const results = []; for (const campaignId of campaignIds) { const resp = await apiRequest(context, 'POST', `/campaigns/${campaignId}/contact/${contactId}/remove`); results.push(resp); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, results)); } default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unsupported operation for contact.', 'Allowed: get, getAll, create, update, delete, sendEmail, addToSegments, removeFromSegments, addToCampaigns, removeFromCampaigns.')); } } // --------------------------------------------------------------------------- // Segment handler (extra operations: addContact, removeContact) // --------------------------------------------------------------------------- async function executeSegmentTool(context, operation, params) { const resource = 'segment'; switch (operation) { case 'get': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric segment ID is required.', "Call mauticadvanced_segment with operation 'getAll' and 'search' to find the segment ID.")); const response = await apiRequest(context, 'GET', `/segments/${params.id}`); const result = response.list; if (isEmpty(result)) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.ENTITY_NOT_FOUND, `No segment found with ID ${params.id}.`, "Call mauticadvanced_segment with operation 'getAll' and 'search' to find the segment.")); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'getAll': { const { limit = 25, ...filterParams } = params; const effectiveLimit = limit; const qs = {}; if (filterParams.search) qs.search = filterParams.search; qs.limit = effectiveLimit; const response = await apiRequest(context, 'GET', '/segments', {}, qs); const records = response.lists ? Object.values(response.lists) : []; const filters = buildFilters(filterParams); const hasFilters = Object.keys(filters).length > 0; if (records.length === 0 && hasFilters) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.NO_RESULTS_FOUND, 'No segment records matched the provided filters.', 'Broaden your search criteria, check for typos, or verify the record exists.', { filtersUsed: filters })); } const resultPayload = { items: records, count: records.length }; if (records.length >= effectiveLimit) { resultPayload.truncated = true; resultPayload.note = `Results capped at ${effectiveLimit}. Increase 'limit' (max 100) if needed.`; } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, resultPayload)); } case 'create': { const body = { ...params }; delete body.id; const response = await apiRequest(context, 'POST', '/segments/new', body); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response.list)); } case 'update': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric segment ID is required.', "Call mauticadvanced_segment with operation 'getAll' and 'search' to find the segment ID.")); const { id, ...body } = params; const response = await apiRequest(context, 'PATCH', `/segments/${id}/edit`, body); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response.list)); } case 'delete': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric segment ID is required.', "Call mauticadvanced_segment with operation 'getAll' and 'search' to find the segment ID.")); await apiRequest(context, 'DELETE', `/segments/${params.id}/delete`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, { id: params.id, deleted: true })); } case 'addContact': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric segment ID is required.', '')); if (!params.contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric contact ID is required.', '')); const response = await apiRequest(context, 'POST', `/segments/${params.id}/contact/${params.contactId}/add`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); } case 'removeContact': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric segment ID is required.', '')); if (!params.contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric contact ID is required.', '')); const response = await apiRequest(context, 'POST', `/segments/${params.id}/contact/${params.contactId}/remove`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); } default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unsupported operation for segment.', 'Allowed: get, getAll, create, update, delete, addContact, removeContact.')); } } // --------------------------------------------------------------------------- // Tag handler (v1/v2 version routing) // --------------------------------------------------------------------------- async function executeTagTool(context, operation, params) { const resource = 'tag'; const mauticVersion = await getMauticVersionFromContext(context); const useV2 = mauticVersion === 'v7'; const tagEndpoint = useV2 ? '/v2/tags' : '/tags'; switch (operation) { case 'get': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric tag ID is required.', "Call mauticadvanced_tag with operation 'getAll' and 'search' to find the tag ID.")); const response = await apiRequest(context, 'GET', `${tagEndpoint}/${params.id}`); const result = useV2 ? normaliseV2Tag(response) : response.tag; if (isEmpty(result)) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.ENTITY_NOT_FOUND, `No tag found with ID ${params.id}.`, "Call mauticadvanced_tag with operation 'getAll' and 'search' to find the tag.")); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'getAll': { const { limit = 25, ...filterParams } = params; const effectiveLimit = limit; const qs = {}; if (filterParams.search) qs.search = filterParams.search; qs.limit = effectiveLimit; qs.orderBy = 'id'; qs.orderByDir = 'asc'; let records; if (useV2) { const response = await apiRequest(context, 'GET', tagEndpoint, {}, qs); records = normaliseV2TagCollection(response); } else { const response = await apiRequest(context, 'GET', tagEndpoint, {}, qs); records = response.tags ? Object.values(response.tags) : []; } const filters = buildFilters(filterParams); const hasFilters = Object.keys(filters).length > 0; if (records.length === 0 && hasFilters) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.NO_RESULTS_FOUND, 'No tag records matched the provided filters.', 'Broaden your search criteria, check for typos, or verify the record exists.', { filtersUsed: filters })); } const resultPayload = { items: records, count: records.length }; if (records.length >= effectiveLimit) { resultPayload.truncated = true; } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, resultPayload)); } case 'create': { const body = {}; if (params.tag) body.tag = params.tag; let response; if (useV2) { const headers = { 'Content-Type': 'application/json' }; response = await GenericFunctions_1.mauticApiRequest.call(context, 'POST', '/v2/tags', body, {}, undefined, headers); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, normaliseV2Tag(response))); } else { response = await apiRequest(context, 'POST', '/tags/new', body); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response.tag)); } } case 'update': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric tag ID is required.', "Call mauticadvanced_tag with operation 'getAll' and 'search' to find the tag ID.")); const { id, ...body } = params; let response; if (useV2) { const headers = { 'Content-Type': 'application/merge-patch+json' }; response = await GenericFunctions_1.mauticApiRequest.call(context, 'PATCH', `/v2/tags/${id}`, body, {}, undefined, headers); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, normaliseV2Tag(response))); } else { response = await apiRequest(context, 'PATCH', `/tags/${id}/edit`, body); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response.tag)); } } case 'delete': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric tag ID is required.', "Call mauticadvanced_tag with operation 'getAll' and 'search' to find the tag ID.")); if (useV2) { await apiRequest(context, 'DELETE', `/v2/tags/${params.id}`); } else { await apiRequest(context, 'DELETE', `/tags/${params.id}/delete`); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, { id: params.id, deleted: true })); } default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unsupported operation for tag.', 'Allowed: get, getAll, create, update, delete.')); } } // Tag v2 normalization helpers (from TagOperations.ts) function normaliseV2Tag(response) { if (!response) return {}; if (response.data && typeof response.data === 'object') { const data = response.data; return { id: data.id, ...(data.attributes || {}) }; } if (Array.isArray(response)) return response[0] || {}; if (response.attributes) return { id: response.id, ...response.attributes }; return response; } function normaliseV2TagCollection(response) { if (!response) return []; if (response.data && Array.isArray(response.data)) { return response.data.map((entry) => normaliseV2Tag({ data: entry })); } if (Array.isArray(response)) return response; if (Array.isArray(response.member)) return response.member; return []; } // --------------------------------------------------------------------------- // Field handler (read-only: get, getAll) // --------------------------------------------------------------------------- async function executeFieldTool(context, operation, params) { const resource = 'field'; const fieldObject = params.fieldObject || 'contact'; switch (operation) { case 'get': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, 'A numeric field ID is required.', '')); const response = await apiRequest(context, 'GET', `/fields/${fieldObject}/${params.id}`); const result = response.field; if (isEmpty(result)) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.ENTITY_NOT_FOUND, `No field found with ID ${params.id}.`, '')); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'getAll': { const limit = params.limit ?? 25; const effectiveLimit = limit; const qs = { limit: effectiveLimit }; const response = await apiRequest(context, 'GET', `/fields/${fieldObject}`, {}, qs); const records = response.fields ? Object.values(response.fields) : []; if (records.length === 0) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.NO_RESULTS_FOUND, `No ${fieldObject} fields found.`, 'Check the fieldObject value (contact or company) and try again.', { filtersUsed: { fieldObject } })); } const resultPayload = { items: records, count: records.length }; if (records.length >= effectiveLimit) { resultPayload.truncated = true; } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, resultPayload)); } default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unsupported operation for field.', 'Allowed: get, getAll.')); } } // --------------------------------------------------------------------------- // Read-only handler (user: get, getAll) // --------------------------------------------------------------------------- async function executeReadOnlyTool(context, resource, operation, params) { const propMap = RESOURCE_PROPERTY_MAP[resource]; const endpoint = RESOURCE_ENDPOINT_MAP[resource]; if (!propMap || !endpoint) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Unknown resource.', '')); } switch (operation) { case 'get': { if (!params.id) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_ENTITY_ID, `A numeric ${resource} ID is required.`, `Call mauticadvanced_${resource} with operation 'getAll' and 'search' to find the ${resource} ID.`)); const response = await apiRequest(context, 'GET', `${endpoint}/${params.id}`); const result = response[propMap.single]; if (isEmpty(result)) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.ENTITY_NOT_FOUND, `No ${resource} found with ID ${params.id}.`, `Call mauticadvanced_${resource} with operation 'getAll' and 'search' to find the ${resource}.`)); } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, result)); } case 'getAll': { const { limit = 25, ...filterParams } = params; const effectiveLimit = limit; const qs = {}; if (filterParams.search) qs.search = filterParams.search; qs.limit = effectiveLimit; qs.orderBy = 'id'; qs.orderByDir = 'asc'; const response = await apiRequest(context, 'GET', endpoint, {}, qs); const rawData = response[propMap.plural]; const records = rawData ? Array.isArray(rawData) ? rawData : Object.values(rawData) : []; const filters = buildFilters(filterParams); const hasFilters = Object.keys(filters).length > 0; if (records.length === 0 && hasFilters) { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.NO_RESULTS_FOUND, `No ${resource} records matched the provided filters.`, 'Broaden your search criteria, check for typos, or verify the record exists.', { filtersUsed: filters })); } const resultPayload = { items: records, count: records.length }; if (records.length >= effectiveLimit) { resultPayload.truncated = true; } return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, resultPayload)); } default: return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, `Unsupported operation for ${resource}.`, `Allowed: get, getAll.`)); } } // --------------------------------------------------------------------------- // Association handlers (companyContact, campaignContact, contactSegment) // --------------------------------------------------------------------------- async function executeCompanyContactTool(context, operation, params) { const resource = 'companyContact'; if (!params.companyId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric company ID is required.', "Call mauticadvanced_company with operation 'getAll' to find the company ID.")); if (!params.contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' to find the contact ID.")); const action = operation === 'add' ? 'add' : 'remove'; const response = await apiRequest(context, 'POST', `/companies/${params.companyId}/contact/${params.contactId}/${action}`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); } async function executeCampaignContactTool(context, operation, params) { const resource = 'campaignContact'; if (!params.campaignId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric campaign ID is required.', "Call mauticadvanced_campaign with operation 'getAll' to find the campaign ID.")); if (!params.contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' to find the contact ID.")); const action = operation === 'add' ? 'add' : 'remove'; const response = await apiRequest(context, 'POST', `/campaigns/${params.campaignId}/contact/${params.contactId}/${action}`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); } async function executeContactSegmentTool(context, operation, params) { const resource = 'contactSegment'; if (!params.segmentId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric segment ID is required.', "Call mauticadvanced_segment with operation 'getAll' to find the segment ID.")); if (!params.contactId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, 'A numeric contact ID is required.', "Call mauticadvanced_contact with operation 'getAll' to find the contact ID.")); const action = operation === 'add' ? 'add' : 'remove'; const response = await apiRequest(context, 'POST', `/segments/${params.segmentId}/contact/${params.contactId}/${action}`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); } // --------------------------------------------------------------------------- // Segment Email handler (send only) // --------------------------------------------------------------------------- async function executeSegmentEmailTool(context, operation, params) { const resource = 'segmentEmail'; if (operation !== 'send') { return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.INVALID_OPERATION, 'Only the send operation is supported for segmentEmail.', '')); } if (!params.emailId) return JSON.stringify((0, error_formatter_1.wrapError)(resource, operation, error_formatter_1.ERROR_TYPES.MISSING_REQUIRED_FIELD, "A numeric email ID is required. Must be a 'list' type email.", "Call mauticadvanced_email with operation 'getAll' and 'search' to find the email ID.")); const response = await apiRequest(context, 'POST', `/emails/${params.emailId}/send`); return JSON.stringify((0, error_formatter_1.wrapSuccess)(resource, operation, response)); }