UNPKG

n8n-nodes-mautic-advanced

Version:

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

420 lines (419 loc) 19.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.executeCompanyOperation = void 0; const n8n_workflow_1 = require("n8n-workflow"); const ApiHelpers_1 = require("../utils/ApiHelpers"); const GenericFunctions_1 = require("../GenericFunctions"); const DataHelpers_1 = require("../utils/DataHelpers"); async function executeCompanyOperation(context, operation, i) { let responseData; try { switch (operation) { case 'create': responseData = await createCompany(context, i); break; case 'update': responseData = await updateCompany(context, i); break; case 'get': responseData = await getCompany(context, i); break; case 'getAll': responseData = await getAllCompanies(context, i); break; case 'delete': responseData = await deleteCompany(context, i); break; default: throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Operation '${operation}' is not supported for Company resource.`, { itemIndex: i }); } return context.helpers.returnJsonArray((0, DataHelpers_1.wrapSingleItem)(responseData)); } catch (error) { return (0, ApiHelpers_1.handleApiError)(context, error, operation, 'Company'); } } exports.executeCompanyOperation = executeCompanyOperation; async function createCompany(context, itemIndex) { const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false); const name = (0, ApiHelpers_1.getRequiredParam)(context, 'name', itemIndex); const mauticVersion = await (0, GenericFunctions_1.getMauticVersion)(context); const additionalFields = (0, ApiHelpers_1.getOptionalParam)(context, 'additionalFields', itemIndex, {}); const { addressUi, customFieldsUi, companyEmail, fax, industry, isPublished, numberOfEmployees, owner, phone, website, annualRevenue, description, ...rest } = additionalFields; const body = {}; if (mauticVersion === 'v7') { body.name = name; if (addressUi?.addressValues) { const { addressValues } = addressUi; if (addressValues.address1) body.address1 = addressValues.address1; if (addressValues.address2) body.address2 = addressValues.address2; if (addressValues.city) body.city = addressValues.city; if (addressValues.state) body.state = addressValues.state; if (addressValues.country) body.country = addressValues.country; if (addressValues.zipCode) body.zipcode = addressValues.zipCode; } if (companyEmail) body.email = companyEmail; if (industry) body.industry = industry; if (isPublished !== undefined) body.isPublished = isPublished; if (owner) body.owner = `/api/v2/users/${owner}`; if (phone) body.phone = phone; if (website) body.website = website; if (description) body.description = description; // fax, numberOfEmployees, annualRevenue not in v7 Company entity write group — omitted } else { body.companyname = name; if (addressUi?.addressValues) { const { addressValues } = addressUi; body.companyaddress1 = addressValues.address1; body.companyaddress2 = addressValues.address2; body.companycity = addressValues.city; body.companystate = addressValues.state; body.companycountry = addressValues.country; body.companyzipcode = addressValues.zipCode; } if (companyEmail) body.companyemail = companyEmail; if (fax) body.companyfax = fax; if (industry) body.companyindustry = industry; if (numberOfEmployees) body.companynumber_of_employees = numberOfEmployees; if (owner) body.owner = owner; if (phone) body.companyphone = phone; if (website) body.companywebsite = website; if (annualRevenue) body.companyannual_revenue = annualRevenue; if (description) body.companydescription = description; } if (customFieldsUi?.customFieldValues) { const { customFieldValues } = customFieldsUi; const data = customFieldValues.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }), {}); Object.assign(body, data); } // v7 API Platform rejects unknown fields; rest only applies to v1 if (mauticVersion !== 'v7') Object.assign(body, rest); let result; if (mauticVersion === 'v7') { const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'POST', '/v2/companies', body, {}, undefined, { 'Content-Type': 'application/json', Accept: 'application/json', }); result = response; } else { const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'POST', '/companies/new', body); result = response.company; } if (simple) { result = toSimpleCompany(result); } return result; } async function updateCompany(context, itemIndex) { const companyId = (0, ApiHelpers_1.getRequiredParam)(context, 'companyId', itemIndex); const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false); const mauticVersion = await (0, GenericFunctions_1.getMauticVersion)(context); const body = {}; const updateFields = (0, ApiHelpers_1.getOptionalParam)(context, 'updateFields', itemIndex, {}); const { addressUi, customFieldsUi, companyEmail, name, fax, industry, isPublished, numberOfEmployees, owner, phone, website, annualRevenue, description, ...rest } = updateFields; if (mauticVersion === 'v7') { if (name) body.name = name; if (addressUi?.addressValues) { const { addressValues } = addressUi; if (addressValues.address1) body.address1 = addressValues.address1; if (addressValues.address2) body.address2 = addressValues.address2; if (addressValues.city) body.city = addressValues.city; if (addressValues.state) body.state = addressValues.state; if (addressValues.country) body.country = addressValues.country; if (addressValues.zipCode) body.zipcode = addressValues.zipCode; } if (companyEmail) body.email = companyEmail; if (industry) body.industry = industry; if (isPublished !== undefined) body.isPublished = isPublished; if (owner) body.owner = `/api/v2/users/${owner}`; if (phone) body.phone = phone; if (website) body.website = website; if (description) body.description = description; // fax, numberOfEmployees, annualRevenue not in v7 Company entity write group — omitted } else { if (name) body.companyname = name; if (addressUi?.addressValues) { const { addressValues } = addressUi; body.companyaddress1 = addressValues.address1; body.companyaddress2 = addressValues.address2; body.companycity = addressValues.city; body.companystate = addressValues.state; body.companycountry = addressValues.country; body.companyzipcode = addressValues.zipCode; } if (companyEmail) body.companyemail = companyEmail; if (fax) body.companyfax = fax; if (industry) body.companyindustry = industry; if (numberOfEmployees) body.companynumber_of_employees = numberOfEmployees; if (owner) body.owner = owner; if (phone) body.companyphone = phone; if (website) body.companywebsite = website; if (annualRevenue) body.companyannual_revenue = annualRevenue; if (description) body.companydescription = description; } if (customFieldsUi?.customFieldValues) { const { customFieldValues } = customFieldsUi; const data = customFieldValues.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }), {}); Object.assign(body, data); } // v7 API Platform rejects unknown fields; rest only applies to v1 if (mauticVersion !== 'v7') Object.assign(body, rest); let result; if (mauticVersion === 'v7') { const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'PATCH', `/v2/companies/${companyId}`, body, {}, undefined, { 'Content-Type': 'application/merge-patch+json', Accept: 'application/json' }); result = response; } else { const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'PATCH', `/companies/${companyId}/edit`, body); result = response.company; } if (simple) { result = toSimpleCompany(result); } return result; } // Logged once per Get/Get Many call when the v2 API exists but rejected the credential, so the // reason owner is null is visible in the n8n logs instead of failing silently. const V2_UNAUTHORIZED_OWNER_WARNING = 'Mautic company owner enrichment skipped: the v2 API (API Platform) rejected this credential (401/403). ' + 'Owner is a v7-only field that requires an auth method the v2 API accepts — Basic auth is confirmed working; ' + 'OAuth2 depends on the Mautic server allowing v2 access for the token. Returning owner: null. ' + 'Switch the credential to Basic auth (or enable v2 access for OAuth2 on the Mautic server) to populate owner.'; // Force API Platform to return JSON-LD/Hydra so the owner is serialised with its `@id` IRI // (/api/v2/users/{id}). The default `application/json` representation omits the IRI, leaving only // FormEntity fields (isPublished/dateAdded/dateModified) with no way to resolve the owner's user ID. const LD_JSON_HEADERS = { Accept: 'application/ld+json' }; // Extract the owner's user ID from a v7 (JSON-LD) company object. // JSON-LD embeds the owner with `@id: "/api/v2/users/{id}"`; some shapes also expose a plain `id`. function extractOwnerFromV7(v7Item) { const owner = v7Item?.owner; if (owner === null || owner === undefined) return null; // Owner may be embedded as an object (with @id), or serialised as a bare IRI string. const iri = typeof owner === 'string' ? owner : owner['@id']; if (iri) { const match = /\/(\d+)$/.exec(iri); if (match) return { id: Number(match[1]) }; } if (typeof owner === 'object' && owner.id !== undefined && owner.id !== null) { return { id: Number(owner.id) }; } // No IRI and no id (plain-JSON owner) — return the partial FormEntity data as-is return owner; } // Requested page size. NOTE: stock Mautic disables client control of page size // (pagination_client_items_per_page = false) and hard-caps at 30, so this is currently a no-op on // default installs — the server returns 30/page regardless. Kept because it is harmless and engages // automatically if an instance enables client page size; termination does not rely on it (we count // actual items returned, see the loop below). const V7_OWNER_PAGE_SIZE = 100; // Backstop only — the loop normally exits when every needed owner is found or the collection ends. const V7_OWNER_MAX_PAGES = 1000; // Read the collection members from a v2 list response, tolerating both the legacy Hydra key // (`hydra:member`) and the newer API Platform 4.x form (`member`), plus a bare JSON array. function getV2CollectionItems(response) { if (Array.isArray(response)) return response; return response?.['hydra:member'] ?? response?.member ?? []; } function getV2TotalItems(response) { const total = response?.['hydra:totalItems'] ?? response?.totalItems; return typeof total === 'number' ? total : undefined; } // Build a map of companyId → owner for ONLY the given company IDs, by paging the v2 collection // (JSON-LD, so the owner `@id` IRI is present) and stopping as soon as every needed owner is // resolved. Mautic's v2 collection defaults to ORDER BY id ASC, so a limited Get Many (whose v1 // result is the lowest ids, also id ASC) resolves its owners in the first page(s) rather than // scanning the whole instance. Returns an empty map when no IDs are needed. async function buildV7OwnerMap(context, neededIds) { const ownerMap = new Map(); if (neededIds.size === 0) return ownerMap; let page = 1; let itemsSeen = 0; while (page <= V7_OWNER_MAX_PAGES) { const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/v2/companies', {}, // order[id]=asc is a no-op on stock Mautic (no OrderFilter on Company), but the v2 collection // already defaults to ORDER BY id ASC, so this just makes the relied-on ordering explicit. { page, itemsPerPage: V7_OWNER_PAGE_SIZE, 'order[id]': 'asc' }, undefined, LD_JSON_HEADERS); const items = getV2CollectionItems(response); if (!items.length) break; itemsSeen += items.length; for (const item of items) { const id = Number(item.id); if (id && neededIds.has(id)) ownerMap.set(id, extractOwnerFromV7(item)); } // Stop once every needed owner is resolved. if (ownerMap.size >= neededIds.size) break; // Stop at the end of the collection (covers needed IDs that no longer exist). Compare against // the actual number of items seen, not an assumed page size — the server may cap itemsPerPage. const total = getV2TotalItems(response); if (total !== undefined && itemsSeen >= total) break; page++; } return ownerMap; } async function getCompany(context, itemIndex) { const companyId = (0, ApiHelpers_1.getRequiredParam)(context, 'companyId', itemIndex); const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false); const v2Status = await (0, GenericFunctions_1.getMauticV2Status)(context); // v1: custom fields via fields.all const v1Response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/companies/${companyId}`); let result = v1Response.company; if (v2Status === 'usable') { try { // JSON-LD response includes the owner @id IRI for user-ID extraction const v7Company = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/v2/companies/${companyId}`, {}, {}, undefined, LD_JSON_HEADERS); result = { ...result, owner: extractOwnerFromV7(v7Company) }; } catch (error) { // Enrichment failed unexpectedly (v2 was usable at probe time). Surface it instead of // silently degrading to owner: null, so the cause is visible in the logs. context.logger.warn(`Mautic company owner enrichment failed for company ${companyId}: ${error?.message ?? error}. Returning owner: null.`); } } else if (v2Status === 'unauthorized') { // v2 route exists but the credential is rejected there — owner cannot be enriched. Warn once. context.logger.warn(V2_UNAUTHORIZED_OWNER_WARNING); } if (simple) result = toSimpleCompany(result); return (0, DataHelpers_1.convertNumericStrings)(result); } async function getAllCompanies(context, itemIndex) { const returnAll = (0, ApiHelpers_1.getOptionalParam)(context, 'returnAll', itemIndex, false); const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false); const v2Status = await (0, GenericFunctions_1.getMauticV2Status)(context); const additionalFields = (0, ApiHelpers_1.getOptionalParam)(context, 'additionalFields', itemIndex, {}); const qs = (0, DataHelpers_1.buildQueryFromOptions)(additionalFields); if (!qs.orderBy) qs.orderBy = 'id'; if (!qs.orderByDir) qs.orderByDir = 'asc'; // v1: custom fields via fields.all let responseData; if (returnAll) { const limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, undefined); responseData = await (0, ApiHelpers_1.makePaginatedRequest)(context, 'companies', 'GET', '/companies', {}, qs, limit); } else { const limit = (0, ApiHelpers_1.getRequiredParam)(context, 'limit', itemIndex); qs.limit = limit; const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/companies', {}, qs); responseData = (response.companies ? Object.values(response.companies) : []); } if (v2Status === 'usable' && responseData.length > 0) { try { // v7 enrichment: resolve owners for ONLY the companies v1 returned, then overlay them. const neededIds = new Set(responseData.map((company) => Number(company.id)).filter((id) => Number.isFinite(id))); const ownerMap = await buildV7OwnerMap(context, neededIds); responseData = responseData.map((company) => ({ ...company, owner: ownerMap.get(Number(company.id)) ?? null, })); } catch (error) { // Enrichment failed unexpectedly (v2 was usable at probe time). Surface it instead of // silently degrading to owner: null, so the cause is visible in the logs. context.logger.warn(`Mautic company owner enrichment failed for Get Many: ${error?.message ?? error}. Returning owner: null for all rows.`); } } else if (v2Status === 'unauthorized') { // v2 route exists but the credential is rejected there — owner cannot be enriched. Warn once. context.logger.warn(V2_UNAUTHORIZED_OWNER_WARNING); } if (simple) { responseData = responseData.map((item) => toSimpleCompany(item)); } return (0, DataHelpers_1.convertNumericStrings)(responseData); } async function deleteCompany(context, itemIndex) { const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false); const companyId = (0, ApiHelpers_1.getRequiredParam)(context, 'companyId', itemIndex); const mauticVersion = await (0, GenericFunctions_1.getMauticVersion)(context); if (mauticVersion === 'v7') { // v7 DELETE returns 204 no body await (0, ApiHelpers_1.makeApiRequest)(context, 'DELETE', `/v2/companies/${companyId}`, {}, {}, undefined, { Accept: 'application/json', }); return { id: Number(companyId) }; } const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'DELETE', `/companies/${companyId}/delete`); let result = response.company; if (simple) { result = toSimpleCompany(result); } return result; } function toSimpleCompany(company) { if (company?.fields?.all) { // v1: fields nested under company.fields.all return { id: company.id, owner: company.owner ?? null, ...company.fields.all, }; } // v7: fields are at the top level; owner is a User object const { id, owner, score, socialCache, dateAdded, dateModified, isPublished, ...fields } = company; return { id, owner: owner ?? null, ...fields, }; }