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