n8n-nodes-mautic-advanced 1.3.2 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -193,6 +193,11 @@ npm install n8n-nodes-mautic-advanced
193
193
  3. Enter your Mautic URL
194
194
  4. Follow the OAuth2 authorization flow
195
195
 
196
+ > **Auth method and Company Owner (v7 enrichment):** The Mautic version (v6 vs v7) is auto-detected by probing the v2 API (API Platform). Company **owner** is a v7-only field, populated by enriching Company *Get* / *Get Many* via the v2 API. The v2 API requires an auth method the server accepts:
197
+ >
198
+ > - **Basic auth** — confirmed working with the v2 API; owner enrichment populates `owner: { id }`.
199
+ > - **OAuth2** — depends on the Mautic server allowing v2 access for the bearer token. Many Mautic installs reject v1-style OAuth2 tokens at the v2 API Platform firewall (HTTP 401), in which case the node falls back to the v1 API for all company operations and **owner cannot be enriched** (`owner: null`). When this happens, a warning is logged explaining the cause. Custom fields and all other company data are unaffected. To populate owner, switch the credential to Basic auth, or enable v2 API access for OAuth2 on the Mautic server.
200
+
196
201
  ## Advanced Features
197
202
 
198
203
  ### Where Filters
@@ -1561,7 +1561,7 @@ exports.contactFields = [
1561
1561
  },
1562
1562
  },
1563
1563
  default: [],
1564
- description: 'Filter contacts by owner (assigned user). Uses Mautic search syntax (owner:id) under the hood.',
1564
+ description: 'Filter contacts by owner (assigned user). Matched on the returned owner ID. Note: Mautic has no server-side owner-by-ID search, so this is applied client-side over the result set — combine with other filters (e.g. segment/tag/DNC) to narrow the set first.',
1565
1565
  },
1566
1566
  {
1567
1567
  displayName: 'Published Only',
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.DEFAULT_MAUTIC_PAGE_SIZE = exports.mauticApiRequest = exports.getMauticVersion = void 0;
3
+ exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.DEFAULT_MAUTIC_PAGE_SIZE = exports.mauticApiRequest = exports.getMauticV2Status = exports.getMauticVersion = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const authenticatedRequest_1 = require("./utils/authenticatedRequest");
6
6
  const versionCache = new Map();
@@ -22,7 +22,7 @@ function normalizeInstanceUrl(raw) {
22
22
  return trimmed.replace(/\/+$/, '');
23
23
  }
24
24
  }
25
- async function getMauticVersion(context) {
25
+ async function detectMauticV2(context) {
26
26
  const authMethod = context.getNodeParameter('authentication', 0, 'credentials');
27
27
  const credentialType = authMethod === 'credentials' ? 'mauticAdvancedApi' : 'mauticAdvancedOAuth2Api';
28
28
  const credentials = await context.getCredentials(credentialType);
@@ -30,23 +30,43 @@ async function getMauticVersion(context) {
30
30
  const cacheKey = `${credentialType}:${baseUrl}`;
31
31
  const cached = versionCache.get(cacheKey);
32
32
  if (cached && Date.now() < cached.expiresAt) {
33
- return cached.version;
33
+ return { version: cached.version, v2Status: cached.v2Status };
34
34
  }
35
- let version;
35
+ let v2Status;
36
36
  try {
37
37
  await mauticApiRequest.call(context, 'GET', '/v2/companies', {}, { page: 1 }, undefined, {
38
38
  Accept: 'application/json',
39
39
  });
40
- version = 'v7';
40
+ v2Status = 'usable';
41
41
  }
42
42
  catch (error) {
43
- // 403 = v2 route exists but no permission → still v7; 404 = route absent → v6
44
- version = error?.httpCode === '403' ? 'v7' : 'v6';
43
+ const httpCode = String(error?.httpCode ?? '');
44
+ // 401/403 = v2 route exists but the credential is rejected/forbidden there. Mautic's v2 API
45
+ // Platform firewall commonly rejects v1-style OAuth2 bearer tokens (Basic auth works). The
46
+ // route exists, but this credential cannot use it — fall back to v1 for routing.
47
+ // 404 / parse errors / network = v2 route absent → genuine v6 instance.
48
+ v2Status = httpCode === '401' || httpCode === '403' ? 'unauthorized' : 'absent';
45
49
  }
46
- versionCache.set(cacheKey, { version, expiresAt: Date.now() + VERSION_CACHE_TTL_MS });
47
- return version;
50
+ // Routing version: only treat as v7 when v2 is actually usable, so that operations route to the
51
+ // v1 endpoints (which work under this credential) whenever v2 is unreachable. Owner enrichment,
52
+ // a v7-only feature, is gated separately on `v2Status === 'usable'`.
53
+ const version = v2Status === 'usable' ? 'v7' : 'v6';
54
+ versionCache.set(cacheKey, { version, v2Status, expiresAt: Date.now() + VERSION_CACHE_TTL_MS });
55
+ return { version, v2Status };
56
+ }
57
+ async function getMauticVersion(context) {
58
+ return (await detectMauticV2(context)).version;
48
59
  }
49
60
  exports.getMauticVersion = getMauticVersion;
61
+ /**
62
+ * Returns whether the Mautic v2 (API Platform) endpoints are usable with the active credential.
63
+ * Callers use this to decide whether v7-only enrichment is possible and to warn actionably when a
64
+ * v2 route exists but the credential is rejected there (`unauthorized`).
65
+ */
66
+ async function getMauticV2Status(context) {
67
+ return (await detectMauticV2(context)).v2Status;
68
+ }
69
+ exports.getMauticV2Status = getMauticV2Status;
50
70
  async function mauticApiRequest(method, endpoint, body = {}, query, uri, headers) {
51
71
  const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials');
52
72
  const options = {
@@ -223,40 +223,86 @@ async function updateCompany(context, itemIndex) {
223
223
  }
224
224
  return result;
225
225
  }
226
- // Extract owner from a v7 company object. Requests without Accept: application/json
227
- // return JSON-LD where the owner IRI (/api/v2/users/{id}) lets us resolve the user ID.
226
+ // Logged once per Get/Get Many call when the v2 API exists but rejected the credential, so the
227
+ // reason owner is null is visible in the n8n logs instead of failing silently.
228
+ const V2_UNAUTHORIZED_OWNER_WARNING = 'Mautic company owner enrichment skipped: the v2 API (API Platform) rejected this credential (401/403). ' +
229
+ 'Owner is a v7-only field that requires an auth method the v2 API accepts — Basic auth is confirmed working; ' +
230
+ 'OAuth2 depends on the Mautic server allowing v2 access for the token. Returning owner: null. ' +
231
+ 'Switch the credential to Basic auth (or enable v2 access for OAuth2 on the Mautic server) to populate owner.';
232
+ // Force API Platform to return JSON-LD/Hydra so the owner is serialised with its `@id` IRI
233
+ // (/api/v2/users/{id}). The default `application/json` representation omits the IRI, leaving only
234
+ // FormEntity fields (isPublished/dateAdded/dateModified) with no way to resolve the owner's user ID.
235
+ const LD_JSON_HEADERS = { Accept: 'application/ld+json' };
236
+ // Extract the owner's user ID from a v7 (JSON-LD) company object.
237
+ // JSON-LD embeds the owner with `@id: "/api/v2/users/{id}"`; some shapes also expose a plain `id`.
228
238
  function extractOwnerFromV7(v7Item) {
229
- if (!v7Item || v7Item.owner === null || v7Item.owner === undefined)
239
+ const owner = v7Item?.owner;
240
+ if (owner === null || owner === undefined)
230
241
  return null;
231
- const iri = v7Item.owner?.['@id'];
242
+ // Owner may be embedded as an object (with @id), or serialised as a bare IRI string.
243
+ const iri = typeof owner === 'string' ? owner : owner['@id'];
232
244
  if (iri) {
233
245
  const match = /\/(\d+)$/.exec(iri);
234
246
  if (match)
235
247
  return { id: Number(match[1]) };
236
248
  }
237
- // No @id (non-Hydra response) return partial FormEntity data as-is
238
- return v7Item.owner;
249
+ if (typeof owner === 'object' && owner.id !== undefined && owner.id !== null) {
250
+ return { id: Number(owner.id) };
251
+ }
252
+ // No IRI and no id (plain-JSON owner) — return the partial FormEntity data as-is
253
+ return owner;
254
+ }
255
+ // Requested page size. NOTE: stock Mautic disables client control of page size
256
+ // (pagination_client_items_per_page = false) and hard-caps at 30, so this is currently a no-op on
257
+ // default installs — the server returns 30/page regardless. Kept because it is harmless and engages
258
+ // automatically if an instance enables client page size; termination does not rely on it (we count
259
+ // actual items returned, see the loop below).
260
+ const V7_OWNER_PAGE_SIZE = 100;
261
+ // Backstop only — the loop normally exits when every needed owner is found or the collection ends.
262
+ const V7_OWNER_MAX_PAGES = 1000;
263
+ // Read the collection members from a v2 list response, tolerating both the legacy Hydra key
264
+ // (`hydra:member`) and the newer API Platform 4.x form (`member`), plus a bare JSON array.
265
+ function getV2CollectionItems(response) {
266
+ if (Array.isArray(response))
267
+ return response;
268
+ return response?.['hydra:member'] ?? response?.member ?? [];
269
+ }
270
+ function getV2TotalItems(response) {
271
+ const total = response?.['hydra:totalItems'] ?? response?.totalItems;
272
+ return typeof total === 'number' ? total : undefined;
239
273
  }
240
- // Fetch all companies from v7 and return a map of companyId owner.
241
- // Called without Accept: application/json so the response includes JSON-LD @id on the owner.
242
- async function buildV7OwnerMap(context) {
274
+ // Build a map of companyId owner for ONLY the given company IDs, by paging the v2 collection
275
+ // (JSON-LD, so the owner `@id` IRI is present) and stopping as soon as every needed owner is
276
+ // resolved. Mautic's v2 collection defaults to ORDER BY id ASC, so a limited Get Many (whose v1
277
+ // result is the lowest ids, also id ASC) resolves its owners in the first page(s) rather than
278
+ // scanning the whole instance. Returns an empty map when no IDs are needed.
279
+ async function buildV7OwnerMap(context, neededIds) {
243
280
  const ownerMap = new Map();
281
+ if (neededIds.size === 0)
282
+ return ownerMap;
244
283
  let page = 1;
245
- const MAX_PAGES = 100;
246
- while (page <= MAX_PAGES) {
247
- const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/v2/companies', {}, { page });
248
- const items = Array.isArray(response)
249
- ? response
250
- : (response?.['hydra:member'] ?? []);
284
+ let itemsSeen = 0;
285
+ while (page <= V7_OWNER_MAX_PAGES) {
286
+ const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/v2/companies', {},
287
+ // order[id]=asc is a no-op on stock Mautic (no OrderFilter on Company), but the v2 collection
288
+ // already defaults to ORDER BY id ASC, so this just makes the relied-on ordering explicit.
289
+ { page, itemsPerPage: V7_OWNER_PAGE_SIZE, 'order[id]': 'asc' }, undefined, LD_JSON_HEADERS);
290
+ const items = getV2CollectionItems(response);
251
291
  if (!items.length)
252
292
  break;
293
+ itemsSeen += items.length;
253
294
  for (const item of items) {
254
295
  const id = Number(item.id);
255
- if (id)
296
+ if (id && neededIds.has(id))
256
297
  ownerMap.set(id, extractOwnerFromV7(item));
257
298
  }
258
- const total = response?.['hydra:totalItems'];
259
- if (typeof total === 'number' && ownerMap.size >= total)
299
+ // Stop once every needed owner is resolved.
300
+ if (ownerMap.size >= neededIds.size)
301
+ break;
302
+ // Stop at the end of the collection (covers needed IDs that no longer exist). Compare against
303
+ // the actual number of items seen, not an assumed page size — the server may cap itemsPerPage.
304
+ const total = getV2TotalItems(response);
305
+ if (total !== undefined && itemsSeen >= total)
260
306
  break;
261
307
  page++;
262
308
  }
@@ -265,20 +311,26 @@ async function buildV7OwnerMap(context) {
265
311
  async function getCompany(context, itemIndex) {
266
312
  const companyId = (0, ApiHelpers_1.getRequiredParam)(context, 'companyId', itemIndex);
267
313
  const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false);
268
- const mauticVersion = await (0, GenericFunctions_1.getMauticVersion)(context);
314
+ const v2Status = await (0, GenericFunctions_1.getMauticV2Status)(context);
269
315
  // v1: custom fields via fields.all
270
316
  const v1Response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/companies/${companyId}`);
271
317
  let result = v1Response.company;
272
- if (mauticVersion === 'v7') {
318
+ if (v2Status === 'usable') {
273
319
  try {
274
- // No Accept: application/json — JSON-LD response includes owner @id for user ID extraction
275
- const v7Company = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/v2/companies/${companyId}`);
320
+ // JSON-LD response includes the owner @id IRI for user-ID extraction
321
+ const v7Company = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/v2/companies/${companyId}`, {}, {}, undefined, LD_JSON_HEADERS);
276
322
  result = { ...result, owner: extractOwnerFromV7(v7Company) };
277
323
  }
278
- catch {
279
- // enrichment failed proceed with v1 data (owner: null)
324
+ catch (error) {
325
+ // Enrichment failed unexpectedly (v2 was usable at probe time). Surface it instead of
326
+ // silently degrading to owner: null, so the cause is visible in the logs.
327
+ context.logger.warn(`Mautic company owner enrichment failed for company ${companyId}: ${error?.message ?? error}. Returning owner: null.`);
280
328
  }
281
329
  }
330
+ else if (v2Status === 'unauthorized') {
331
+ // v2 route exists but the credential is rejected there — owner cannot be enriched. Warn once.
332
+ context.logger.warn(V2_UNAUTHORIZED_OWNER_WARNING);
333
+ }
282
334
  if (simple)
283
335
  result = toSimpleCompany(result);
284
336
  return (0, DataHelpers_1.convertNumericStrings)(result);
@@ -286,7 +338,7 @@ async function getCompany(context, itemIndex) {
286
338
  async function getAllCompanies(context, itemIndex) {
287
339
  const returnAll = (0, ApiHelpers_1.getOptionalParam)(context, 'returnAll', itemIndex, false);
288
340
  const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false);
289
- const mauticVersion = await (0, GenericFunctions_1.getMauticVersion)(context);
341
+ const v2Status = await (0, GenericFunctions_1.getMauticV2Status)(context);
290
342
  const additionalFields = (0, ApiHelpers_1.getOptionalParam)(context, 'additionalFields', itemIndex, {});
291
343
  const qs = (0, DataHelpers_1.buildQueryFromOptions)(additionalFields);
292
344
  if (!qs.orderBy)
@@ -305,19 +357,26 @@ async function getAllCompanies(context, itemIndex) {
305
357
  const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/companies', {}, qs);
306
358
  responseData = (response.companies ? Object.values(response.companies) : []);
307
359
  }
308
- if (mauticVersion === 'v7') {
360
+ if (v2Status === 'usable' && responseData.length > 0) {
309
361
  try {
310
- // v7 enrichment: overlay owner on each v1 result using a single paginated v7 fetch
311
- const ownerMap = await buildV7OwnerMap(context);
362
+ // v7 enrichment: resolve owners for ONLY the companies v1 returned, then overlay them.
363
+ const neededIds = new Set(responseData.map((company) => Number(company.id)).filter((id) => Number.isFinite(id)));
364
+ const ownerMap = await buildV7OwnerMap(context, neededIds);
312
365
  responseData = responseData.map((company) => ({
313
366
  ...company,
314
367
  owner: ownerMap.get(Number(company.id)) ?? null,
315
368
  }));
316
369
  }
317
- catch {
318
- // enrichment failed proceed with v1 data (owner: null)
370
+ catch (error) {
371
+ // Enrichment failed unexpectedly (v2 was usable at probe time). Surface it instead of
372
+ // silently degrading to owner: null, so the cause is visible in the logs.
373
+ context.logger.warn(`Mautic company owner enrichment failed for Get Many: ${error?.message ?? error}. Returning owner: null for all rows.`);
319
374
  }
320
375
  }
376
+ else if (v2Status === 'unauthorized') {
377
+ // v2 route exists but the credential is rejected there — owner cannot be enriched. Warn once.
378
+ context.logger.warn(V2_UNAUTHORIZED_OWNER_WARNING);
379
+ }
321
380
  if (simple) {
322
381
  responseData = responseData.map((item) => toSimpleCompany(item));
323
382
  }
@@ -191,12 +191,34 @@ async function getAllContacts(context, itemIndex) {
191
191
  if (filterExpr)
192
192
  searchParts.push(filterExpr);
193
193
  }
194
- // Owner filter (always OR for multiple owners - a contact can only have one owner)
194
+ // Owner filter applied CLIENT-SIDE on the contact's owner.id.
195
+ // Mautic's `owner:` search command matches the owner's first/last NAME (LIKE), not the user id,
196
+ // so sending `owner:<id>` (what the Owner(s) dropdown provides) silently returns nothing. The v1
197
+ // contact body already includes `owner.id`, so we filter on that instead.
195
198
  const owners = options.owners;
196
- if (Array.isArray(owners) && owners.length > 0) {
197
- const filterExpr = buildSearchFilterExpression(owners, 'owner', 'any');
198
- if (filterExpr)
199
- searchParts.push(filterExpr);
199
+ const ownerIdFilter = Array.isArray(owners) && owners.length > 0
200
+ ? new Set(owners.map((o) => Number(o)).filter((n) => Number.isFinite(n)))
201
+ : undefined;
202
+ // Do-Not-Contact filter — applied SERVER-SIDE via Mautic's `dnc:` search command, so only
203
+ // matching contacts are fetched (replaces the previous client-side page-and-discard scan).
204
+ // `dnc:<channel>` = IS on DNC for that channel; `dnc:any` = on any channel.
205
+ const emailDncOnly = options.emailDncOnly === true;
206
+ const smsDncOnly = options.smsDncOnly === true;
207
+ const anyDncOnly = options.anyDncOnly === true;
208
+ if (anyDncOnly) {
209
+ searchParts.push('dnc:any');
210
+ }
211
+ else {
212
+ const dncTokens = [];
213
+ if (emailDncOnly)
214
+ dncTokens.push('dnc:email');
215
+ if (smsDncOnly)
216
+ dncTokens.push('dnc:sms');
217
+ // Multiple specific channels = union (on email OR sms DNC); one token if only one is set.
218
+ if (dncTokens.length === 1)
219
+ searchParts.push(dncTokens[0]);
220
+ else if (dncTokens.length > 1)
221
+ searchParts.push(dncTokens.join(' OR '));
200
222
  }
201
223
  // Stage filter (always OR for multiple stages - a contact can only be in one stage)
202
224
  const stages = options.stages;
@@ -241,23 +263,19 @@ async function getAllContacts(context, itemIndex) {
241
263
  }
242
264
  }
243
265
  let responseData;
244
- const emailDncOnly = options.emailDncOnly === true;
245
- const smsDncOnly = options.smsDncOnly === true;
246
- const anyDncOnly = options.anyDncOnly === true;
247
- const useDncPostFilter = emailDncOnly || smsDncOnly || anyDncOnly;
248
- if (useDncPostFilter) {
266
+ if (ownerIdFilter) {
267
+ // Owner filter has no server-side equivalent, so page through the (already server-filtered)
268
+ // results and keep only those whose owner.id matches, until the limit is met or results run out.
249
269
  const limit = returnAll ? undefined : (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
250
- responseData = await getContactsWithDncFilter(context, qs, options, limit);
270
+ responseData = await getContactsWithClientFilter(context, qs, (contact) => ownerIdFilter.has(Number(contact?.owner?.id)), limit);
271
+ }
272
+ else if (returnAll) {
273
+ responseData = await (0, ApiHelpers_1.makePaginatedRequest)(context, 'contacts', 'GET', '/contacts', {}, qs);
251
274
  }
252
275
  else {
253
- if (returnAll) {
254
- responseData = await (0, ApiHelpers_1.makePaginatedRequest)(context, 'contacts', 'GET', '/contacts', {}, qs);
255
- }
256
- else {
257
- qs.limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
258
- const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, qs);
259
- responseData = response.contacts ? Object.values(response.contacts) : [];
260
- }
276
+ qs.limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
277
+ const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, qs);
278
+ responseData = response.contacts ? Object.values(response.contacts) : [];
261
279
  }
262
280
  const processedData = (0, DataHelpers_1.processContactFields)(responseData, options, options.fieldsToReturn);
263
281
  return (0, DataHelpers_1.convertNumericStrings)(processedData);
@@ -620,34 +638,22 @@ function addContactFields(body, fields) {
620
638
  Object.assign(body, data);
621
639
  }
622
640
  }
623
- async function getContactsWithDncFilter(context, qs, options, limit) {
624
- const emailDncOnly = options.emailDncOnly === true;
625
- const smsDncOnly = options.smsDncOnly === true;
626
- const anyDncOnly = options.anyDncOnly === true;
641
+ // Page through /contacts (already narrowed by any server-side search in `qs`) and keep only the
642
+ // contacts that satisfy `predicate`, stopping once `limit` matches are collected or results run out.
643
+ // Used for filters that have no server-side equivalent (e.g. owner-by-id).
644
+ async function getContactsWithClientFilter(context, qs, predicate, limit) {
627
645
  const requestedStart = Number(qs.start ?? 0);
628
646
  const maxResults = typeof limit === 'number' && limit > 0 ? Math.floor(limit) : undefined;
629
647
  const contacts = [];
630
648
  let remaining = maxResults;
631
649
  let currentStart = Number.isFinite(requestedStart) && requestedStart > 0 ? requestedStart : 0;
632
650
  while (remaining === undefined || remaining > 0) {
633
- const pageLimit = remaining === undefined
634
- ? GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE
635
- : Math.min(remaining, GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE);
651
+ // Always fetch full pages so the scan terminates promptly; `remaining` only caps what we keep.
652
+ const pageLimit = GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE;
636
653
  const pageQs = { ...qs, start: currentStart, limit: pageLimit };
637
654
  const pageResponse = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, pageQs);
638
655
  const pageContacts = pageResponse.contacts ? Object.values(pageResponse.contacts) : [];
639
- const filtered = pageContacts.filter((contact) => {
640
- const dnc = contact.doNotContact || [];
641
- const hasEmailDnc = dnc.some((d) => d.channel === 'email');
642
- const hasSmsDnc = dnc.some((d) => d.channel === 'sms');
643
- if (emailDncOnly)
644
- return hasEmailDnc;
645
- if (smsDncOnly)
646
- return hasSmsDnc;
647
- if (anyDncOnly)
648
- return hasEmailDnc || hasSmsDnc;
649
- return true;
650
- });
656
+ const filtered = pageContacts.filter(predicate);
651
657
  const toAdd = remaining === undefined ? filtered : filtered.slice(0, remaining);
652
658
  contacts.push(...toAdd);
653
659
  if (remaining !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-mautic-advanced",
3
- "version": "1.3.2",
3
+ "version": "1.3.7",
4
4
  "description": "Enhanced n8n node for Mautic with comprehensive API coverage including tags, campaigns, categories, and advanced contact management",
5
5
  "keywords": [
6
6
  "n8n",